diff --git a/Editor/AGS.Editor/Resources/agsdefns.sh b/Editor/AGS.Editor/Resources/agsdefns.sh index cad38227545..e10786803d4 100644 --- a/Editor/AGS.Editor/Resources/agsdefns.sh +++ b/Editor/AGS.Editor/Resources/agsdefns.sh @@ -2659,6 +2659,51 @@ builtin managed struct Joystick { }; #endif +#ifdef SCRIPT_API_v400 +enum PlaybackState { + ePlaybackOn = 2, + ePlaybackPaused = 3, + ePlaybackStopped = 4 +}; + +builtin managed struct VideoPlayer { + import static VideoPlayer* Open(const string filename, bool autoPlay=true, RepeatStyle=eOnce); + /// Starts or resumes the playback. + import void Play(); + /// Pauses the playback. + import void Pause(); + /// Advances video by 1 frame, may be called when the video is paused. + import void NextFrame(); + /// Changes playback to continue from the specified frame; returns new position or -1 on error. + import int SeekFrame(int frame); + /// Changes playback to continue from the specified position in milliseconds; returns new position or -1 on error. + import int SeekMs(int position); + /// Stops the video completely. + import void Stop(); + + /// Gets current frame index. + import readonly attribute int Frame; + /// Gets total number of frames in this video. + import readonly attribute int FrameCount; + /// Gets this video's framerate (number of frames per second). + import readonly attribute float FrameRate; + /// Gets the number of sprite this video renders to. + import readonly attribute int Graphic; + /// The length of the currently playing video, in milliseconds. + import readonly attribute int LengthMs; + /// Gets/sets whether the video should loop. + import attribute bool Looping; + /// The current playback position, in milliseconds. + import readonly attribute int PositionMs; + /// The speed of playing (1.0 is default). + import attribute float Speed; + /// Gets the current playback state (playing, paused, etc). + import readonly attribute PlaybackState State; + /// The volume of this video's sound, from 0 to 100. + import attribute int Volume; +}; +#endif + import ColorType palette[PALETTE_SIZE]; import Mouse mouse; diff --git a/Engine/CMakeLists.txt b/Engine/CMakeLists.txt index 5ad846c2567..3f4cd18e266 100644 --- a/Engine/CMakeLists.txt +++ b/Engine/CMakeLists.txt @@ -117,6 +117,8 @@ target_sources(engine ac/dynobj/scriptsystem.cpp ac/dynobj/scriptuserobject.cpp ac/dynobj/scriptuserobject.h + ac/dynobj/scriptvideoplayer.cpp + ac/dynobj/scriptvideoplayer.h ac/dynobj/scriptviewframe.cpp ac/dynobj/scriptviewframe.h ac/dynobj/scriptviewport.cpp @@ -248,6 +250,7 @@ target_sources(engine ac/timer.h ac/translation.cpp ac/translation.h + ac/video_script.cpp ac/viewframe.cpp ac/viewframe.h ac/viewport_script.cpp @@ -375,6 +378,8 @@ target_sources(engine media/video/theora_player.h media/video/video.cpp media/video/video.h + media/video/video_core.cpp + media/video/video_core.h media/video/videoplayer.cpp media/video/videoplayer.h platform/base/agsplatformdriver.cpp @@ -415,6 +420,7 @@ target_sources(engine util/library_posix.h util/sdl2_util.h util/sdl2_util.cpp + util/threading.h platform/windows/acplwin.cpp platform/windows/debug/namedpipesagsdebugger.cpp diff --git a/Engine/ac/dynobj/scriptvideoplayer.cpp b/Engine/ac/dynobj/scriptvideoplayer.cpp new file mode 100644 index 00000000000..b6e6037a422 --- /dev/null +++ b/Engine/ac/dynobj/scriptvideoplayer.cpp @@ -0,0 +1,51 @@ +//============================================================================= +// +// Adventure Game Studio (AGS) +// +// Copyright (C) 1999-2011 Chris Jones and 2011-2024 various contributors +// The full list of copyright holders can be found in the Copyright.txt +// file, which is part of this source code distribution. +// +// The AGS source code is provided under the Artistic License 2.0. +// A copy of this license can be found in the file License.txt and at +// https://opensource.org/license/artistic-2-0/ +// +//============================================================================= +#include "ac/dynobj/scriptvideoplayer.h" +#include "ac/dynobj/dynobj_manager.h" +#include "media/video/video.h" +#include "util/stream.h" + +using namespace AGS::Common; + +const char *ScriptVideoPlayer::GetType() +{ + return "VideoPlayer"; +} + +int ScriptVideoPlayer::Dispose(void* /*address*/, bool /*force*/) +{ + if (_id >= 0) + { + video_stop(_id); + } + + delete this; + return 1; +} + +size_t ScriptVideoPlayer::CalcSerializeSize(const void* /*address*/) +{ + return sizeof(int32_t); +} + +void ScriptVideoPlayer::Serialize(const void* /*address*/, Stream *out) +{ + out->WriteInt32(_id); +} + +void ScriptVideoPlayer::Unserialize(int index, Stream *in, size_t /*data_sz*/) +{ + _id = in->ReadInt32(); + ccRegisterUnserializedObject(index, this, this); +} diff --git a/Engine/ac/dynobj/scriptvideoplayer.h b/Engine/ac/dynobj/scriptvideoplayer.h new file mode 100644 index 00000000000..f0bc48d0c78 --- /dev/null +++ b/Engine/ac/dynobj/scriptvideoplayer.h @@ -0,0 +1,41 @@ +//============================================================================= +// +// Adventure Game Studio (AGS) +// +// Copyright (C) 1999-2011 Chris Jones and 2011-2024 various contributors +// The full list of copyright holders can be found in the Copyright.txt +// file, which is part of this source code distribution. +// +// The AGS source code is provided under the Artistic License 2.0. +// A copy of this license can be found in the file License.txt and at +// https://opensource.org/license/artistic-2-0/ +// +//============================================================================= +#ifndef __AC_SCRIPTVIDEOPLAYER_H +#define __AC_SCRIPTVIDEOPLAYER_H + +#include "ac/dynobj/cc_agsdynamicobject.h" + +struct ScriptVideoPlayer final : AGSCCDynamicObject +{ +public: + ScriptVideoPlayer(int id) : _id(id) {} + // Get videoplayer's index; negative means the video was removed + int GetID() const { return _id; } + // Reset videoplayer's index to indicate that this reference is no longer valid + void Invalidate() { _id = -1; } + + const char *GetType() override; + int Dispose(void *address, bool force) override; + void Unserialize(int index, AGS::Common::Stream *in, size_t data_sz) override; + +private: + // Calculate and return required space for serialization, in bytes + size_t CalcSerializeSize(const void *address) override; + // Write object data into the provided stream + void Serialize(const void *address, AGS::Common::Stream *out) override; + + int _id = -1; // index of videoplayer in the video subsystem +}; + +#endif // __AC_SCRIPTVIDEOPLAYER_H diff --git a/Engine/ac/global_video.cpp b/Engine/ac/global_video.cpp index a30746a8dc0..7369989bf49 100644 --- a/Engine/ac/global_video.cpp +++ b/Engine/ac/global_video.cpp @@ -18,6 +18,7 @@ #include "debug/debugger.h" #include "debug/debug_log.h" #include "media/video/video.h" +#include "media/video/videoplayer.h" // for flags using namespace AGS::Common; using namespace AGS::Engine; diff --git a/Engine/ac/timer.cpp b/Engine/ac/timer.cpp index df99bb14039..263eed67421 100644 --- a/Engine/ac/timer.cpp +++ b/Engine/ac/timer.cpp @@ -18,6 +18,7 @@ #include "platform/base/agsplatformdriver.h" #if defined(AGS_DISABLE_THREADS) #include "media/audio/audio_core.h" +#include "media/video/video_core.h" #endif #if AGS_PLATFORM_OS_EMSCRIPTEN #include "SDL.h" @@ -71,6 +72,7 @@ void WaitForNextFrame() // Do the last polls on this frame, if necessary #if defined(AGS_DISABLE_THREADS) audio_core_entry_poll(); + video_core_entry_poll(); #endif const auto now = AGS_Clock::now(); diff --git a/Engine/ac/video_script.cpp b/Engine/ac/video_script.cpp new file mode 100644 index 00000000000..8f502571e3e --- /dev/null +++ b/Engine/ac/video_script.cpp @@ -0,0 +1,336 @@ +//============================================================================= +// +// Adventure Game Studio (AGS) +// +// Copyright (C) 1999-2011 Chris Jones and 2011-2024 various contributors +// The full list of copyright holders can be found in the Copyright.txt +// file, which is part of this source code distribution. +// +// The AGS source code is provided under the Artistic License 2.0. +// A copy of this license can be found in the file License.txt and at +// https://opensource.org/license/artistic-2-0/ +// +//============================================================================= +// +// VideoPlayer script API. +// +//============================================================================= +#include "ac/dynobj/dynobj_manager.h" +#include "ac/dynobj/scriptvideoplayer.h" +#include "debug/debug_log.h" +#include "media/video/video.h" +#include "media/video/videoplayer.h" +#include "script/script_api.h" +#include "script/script_runtime.h" + +using namespace AGS::Common; +using namespace AGS::Engine; + +ScriptVideoPlayer *VideoPlayer_Open(const char *filename, bool auto_play, int repeat_style) +{ + int video_id; + HError err = open_video(filename, + kVideo_EnableVideo | kVideo_EnableAudio | kVideo_DropFrames, video_id); + if (!err) + { + debug_script_warn("Failed to play video '%s': %s", filename, err->FullMessage().GetCStr()); + return nullptr; + } + + VideoControl *video_ctrl = get_video_control(video_id); + video_ctrl->SetLooping((repeat_style) ? true : false); + if (auto_play) + { + video_ctrl->Play(); + } + + ScriptVideoPlayer *sc_video = new ScriptVideoPlayer(video_id); + int handle = ccRegisterManagedObject(sc_video, sc_video); + video_ctrl->SetScriptHandle(handle); + return sc_video; +} + +void VideoPlayer_Play(ScriptVideoPlayer *sc_video) +{ + if (sc_video->GetID() < 0) + return; + VideoControl *video_ctrl = get_video_control(sc_video->GetID()); + video_ctrl->Play(); +} + +void VideoPlayer_Pause(ScriptVideoPlayer *sc_video) +{ + if (sc_video->GetID() < 0) + return; + VideoControl *video_ctrl = get_video_control(sc_video->GetID()); + video_ctrl->Pause(); +} + +void VideoPlayer_NextFrame(ScriptVideoPlayer *sc_video) +{ + if (sc_video->GetID() < 0) + return; + VideoControl *video_ctrl = get_video_control(sc_video->GetID()); + video_ctrl->NextFrame(); +} + +int VideoPlayer_SeekFrame(ScriptVideoPlayer *sc_video, int frame) +{ + if (sc_video->GetID() < 0) + return 0; + VideoControl *video_ctrl = get_video_control(sc_video->GetID()); + return video_ctrl->SeekFrame(frame); +} + +int VideoPlayer_SeekMs(ScriptVideoPlayer *sc_video, int pos) +{ + if (sc_video->GetID() < 0) + return 0; + VideoControl *video_ctrl = get_video_control(sc_video->GetID()); + return video_ctrl->SeekMs(pos); +} + +void VideoPlayer_Stop(ScriptVideoPlayer *sc_video) +{ + if (sc_video->GetID() < 0) + return; + video_stop(sc_video->GetID()); + sc_video->Invalidate(); +} + +int VideoPlayer_GetFrame(ScriptVideoPlayer *sc_video) +{ + if (sc_video->GetID() < 0) + return 0; + VideoControl *video_ctrl = get_video_control(sc_video->GetID()); + return video_ctrl->GetFrame(); +} + +int VideoPlayer_GetFrameCount(ScriptVideoPlayer *sc_video) +{ + if (sc_video->GetID() < 0) + return 0; + VideoControl *video_ctrl = get_video_control(sc_video->GetID()); + return video_ctrl->GetFrameCount(); +} + +float VideoPlayer_GetFrameRate(ScriptVideoPlayer *sc_video) +{ + if (sc_video->GetID() < 0) + return 0.f; + VideoControl *video_ctrl = get_video_control(sc_video->GetID()); + return video_ctrl->GetFrameRate(); +} + +int VideoPlayer_GetGraphic(ScriptVideoPlayer *sc_video) +{ + if (sc_video->GetID() < 0) + return 0; + VideoControl *video_ctrl = get_video_control(sc_video->GetID()); + return video_ctrl->GetSpriteID(); +} + +int VideoPlayer_GetLengthMs(ScriptVideoPlayer *sc_video) +{ + if (sc_video->GetID() < 0) + return 0; + VideoControl *video_ctrl = get_video_control(sc_video->GetID()); + return video_ctrl->GetDurationMs(); +} + +int VideoPlayer_GetLooping(ScriptVideoPlayer *sc_video) +{ + if (sc_video->GetID() < 0) + return false; + VideoControl *video_ctrl = get_video_control(sc_video->GetID()); + return video_ctrl->GetLooping(); +} + +void VideoPlayer_SetLooping(ScriptVideoPlayer *sc_video, bool loop) +{ + if (sc_video->GetID() < 0) + return; + VideoControl *video_ctrl = get_video_control(sc_video->GetID()); + return video_ctrl->SetLooping(loop); +} + +int VideoPlayer_GetPositionMs(ScriptVideoPlayer *sc_video) +{ + if (sc_video->GetID() < 0) + return 0; + VideoControl *video_ctrl = get_video_control(sc_video->GetID()); + return video_ctrl->GetPositionMs(); +} + +float VideoPlayer_GetSpeed(ScriptVideoPlayer *sc_video) +{ + if (sc_video->GetID() < 0) + return 0.f; + VideoControl *video_ctrl = get_video_control(sc_video->GetID()); + return video_ctrl->GetSpeed(); +} + +void VideoPlayer_SetSpeed(ScriptVideoPlayer *sc_video, float speed) +{ + if (sc_video->GetID() < 0) + return; + VideoControl *video_ctrl = get_video_control(sc_video->GetID()); + video_ctrl->SetSpeed(speed); +} + +int VideoPlayer_GetState(ScriptVideoPlayer *sc_video) +{ + if (sc_video->GetID() < 0) + return PlaybackState::PlayStateInvalid; + VideoControl *video_ctrl = get_video_control(sc_video->GetID()); + return video_ctrl->GetState(); +} + +int VideoPlayer_GetVolume(ScriptVideoPlayer *sc_video) +{ + if (sc_video->GetID() < 0) + return 0; + VideoControl *video_ctrl = get_video_control(sc_video->GetID()); + return video_ctrl->GetVolume(); +} + +void VideoPlayer_SetVolume(ScriptVideoPlayer *sc_video, int volume) +{ + if (sc_video->GetID() < 0) + return; + VideoControl *video_ctrl = get_video_control(sc_video->GetID()); + video_ctrl->SetVolume(volume); +} + +//============================================================================= + +RuntimeScriptValue Sc_VideoPlayer_Open(const RuntimeScriptValue *params, int32_t param_count) +{ + API_SCALL_OBJAUTO_POBJ_PINT2(ScriptVideoPlayer, VideoPlayer_Open, const char); +} + +RuntimeScriptValue Sc_VideoPlayer_Play(void *self, const RuntimeScriptValue *params, int32_t param_count) +{ + API_OBJCALL_VOID(ScriptVideoPlayer, VideoPlayer_Play); +} + +RuntimeScriptValue Sc_VideoPlayer_Pause(void *self, const RuntimeScriptValue *params, int32_t param_count) +{ + API_OBJCALL_VOID(ScriptVideoPlayer, VideoPlayer_Pause); +} + +RuntimeScriptValue Sc_VideoPlayer_NextFrame(void *self, const RuntimeScriptValue *params, int32_t param_count) +{ + API_OBJCALL_VOID(ScriptVideoPlayer, VideoPlayer_NextFrame); +} + +RuntimeScriptValue Sc_VideoPlayer_SeekFrame(void *self, const RuntimeScriptValue *params, int32_t param_count) +{ + API_OBJCALL_INT_PINT(ScriptVideoPlayer, VideoPlayer_SeekFrame); +} + +RuntimeScriptValue Sc_VideoPlayer_SeekMs(void *self, const RuntimeScriptValue *params, int32_t param_count) +{ + API_OBJCALL_INT_PINT(ScriptVideoPlayer, VideoPlayer_SeekMs); +} + +RuntimeScriptValue Sc_VideoPlayer_Stop(void *self, const RuntimeScriptValue *params, int32_t param_count) +{ + API_OBJCALL_VOID(ScriptVideoPlayer, VideoPlayer_Stop); +} + +RuntimeScriptValue Sc_VideoPlayer_GetFrame(void *self, const RuntimeScriptValue *params, int32_t param_count) +{ + API_OBJCALL_INT(ScriptVideoPlayer, VideoPlayer_GetFrame); +} + +RuntimeScriptValue Sc_VideoPlayer_GetFrameCount(void *self, const RuntimeScriptValue *params, int32_t param_count) +{ + API_OBJCALL_INT(ScriptVideoPlayer, VideoPlayer_GetFrameCount); +} + +RuntimeScriptValue Sc_VideoPlayer_GetFrameRate(void *self, const RuntimeScriptValue *params, int32_t param_count) +{ + API_OBJCALL_FLOAT(ScriptVideoPlayer, VideoPlayer_GetFrameRate); +} + +RuntimeScriptValue Sc_VideoPlayer_GetGraphic(void *self, const RuntimeScriptValue *params, int32_t param_count) +{ + API_OBJCALL_INT(ScriptVideoPlayer, VideoPlayer_GetGraphic); +} + +RuntimeScriptValue Sc_VideoPlayer_GetLengthMs(void *self, const RuntimeScriptValue *params, int32_t param_count) +{ + API_OBJCALL_INT(ScriptVideoPlayer, VideoPlayer_GetLengthMs); +} + +RuntimeScriptValue Sc_VideoPlayer_GetLooping(void *self, const RuntimeScriptValue *params, int32_t param_count) +{ + API_OBJCALL_INT(ScriptVideoPlayer, VideoPlayer_GetLooping); +} + +RuntimeScriptValue Sc_VideoPlayer_SetLooping(void *self, const RuntimeScriptValue *params, int32_t param_count) +{ + API_OBJCALL_VOID_PBOOL(ScriptVideoPlayer, VideoPlayer_SetLooping); +} + +RuntimeScriptValue Sc_VideoPlayer_GetPositionMs(void *self, const RuntimeScriptValue *params, int32_t param_count) +{ + API_OBJCALL_INT(ScriptVideoPlayer, VideoPlayer_GetPositionMs); +} + +RuntimeScriptValue Sc_VideoPlayer_GetSpeed(void *self, const RuntimeScriptValue *params, int32_t param_count) +{ + API_OBJCALL_FLOAT(ScriptVideoPlayer, VideoPlayer_GetSpeed); +} + +RuntimeScriptValue Sc_VideoPlayer_SetSpeed(void *self, const RuntimeScriptValue *params, int32_t param_count) +{ + API_OBJCALL_VOID_PFLOAT(ScriptVideoPlayer, VideoPlayer_SetSpeed); +} + +RuntimeScriptValue Sc_VideoPlayer_GetState(void *self, const RuntimeScriptValue *params, int32_t param_count) +{ + API_OBJCALL_INT(ScriptVideoPlayer, VideoPlayer_GetState); +} + +RuntimeScriptValue Sc_VideoPlayer_GetVolume(void *self, const RuntimeScriptValue *params, int32_t param_count) +{ + API_OBJCALL_INT(ScriptVideoPlayer, VideoPlayer_GetVolume); +} + +RuntimeScriptValue Sc_VideoPlayer_SetVolume(void *self, const RuntimeScriptValue *params, int32_t param_count) +{ + API_OBJCALL_VOID_PINT(ScriptVideoPlayer, VideoPlayer_SetVolume); +} + + + +void RegisterVideoAPI() +{ + ScFnRegister video_api[] = { + { "VideoPlayer::Open", API_FN_PAIR(VideoPlayer_Open) }, + { "VideoPlayer::Play", API_FN_PAIR(VideoPlayer_Play) }, + { "VideoPlayer::Pause", API_FN_PAIR(VideoPlayer_Pause) }, + { "VideoPlayer::NextFrame", API_FN_PAIR(VideoPlayer_NextFrame) }, + { "VideoPlayer::SeekFrame", API_FN_PAIR(VideoPlayer_SeekFrame) }, + { "VideoPlayer::SeekMs", API_FN_PAIR(VideoPlayer_SeekMs) }, + { "VideoPlayer::Stop", API_FN_PAIR(VideoPlayer_Stop) }, + + { "VideoPlayer::get_Frame", API_FN_PAIR(VideoPlayer_GetFrame) }, + { "VideoPlayer::get_FrameCount", API_FN_PAIR(VideoPlayer_GetFrameCount) }, + { "VideoPlayer::get_FrameRate", API_FN_PAIR(VideoPlayer_GetFrameRate) }, + { "VideoPlayer::get_Graphic", API_FN_PAIR(VideoPlayer_GetGraphic) }, + { "VideoPlayer::get_LengthMs", API_FN_PAIR(VideoPlayer_GetLengthMs) }, + { "VideoPlayer::get_Looping", API_FN_PAIR(VideoPlayer_GetLooping) }, + { "VideoPlayer::set_Looping", API_FN_PAIR(VideoPlayer_SetLooping) }, + { "VideoPlayer::get_PositionMs", API_FN_PAIR(VideoPlayer_GetPositionMs) }, + { "VideoPlayer::get_Speed", API_FN_PAIR(VideoPlayer_GetSpeed) }, + { "VideoPlayer::set_Speed", API_FN_PAIR(VideoPlayer_SetSpeed) }, + { "VideoPlayer::get_State", API_FN_PAIR(VideoPlayer_GetState) }, + { "VideoPlayer::get_Volume", API_FN_PAIR(VideoPlayer_GetVolume) }, + { "VideoPlayer::set_Volume", API_FN_PAIR(VideoPlayer_SetVolume) } + }; + + ccAddExternalFunctions(video_api); +} diff --git a/Engine/main/engine_setup.cpp b/Engine/main/engine_setup.cpp index f4a304e71b6..d3e86a7a6aa 100644 --- a/Engine/main/engine_setup.cpp +++ b/Engine/main/engine_setup.cpp @@ -11,6 +11,7 @@ // https://opensource.org/license/artistic-2-0/ // //============================================================================= +#include #include "core/platform.h" #include "ac/common.h" #include "ac/display.h" diff --git a/Engine/main/game_run.cpp b/Engine/main/game_run.cpp index 1356cce1583..a001776a811 100644 --- a/Engine/main/game_run.cpp +++ b/Engine/main/game_run.cpp @@ -60,6 +60,7 @@ #include "main/game_run.h" #include "main/update.h" #include "media/audio/audio_system.h" +#include "media/video/video.h" #include "platform/base/agsplatformdriver.h" #include "plugin/agsplugin_evts.h" #include "plugin/plugin_engine.h" @@ -1092,6 +1093,7 @@ void UpdateGameOnce(bool checkControls, IDriverDependantBitmap *extraBitmap, int update_cursor_over_location(mwasatx, mwasaty); update_cursor_view(); + update_video_system_on_game_loop(); update_audio_system_on_game_loop(); // Only render if we are not skipping a cutscene diff --git a/Engine/media/audio/audio.cpp b/Engine/media/audio/audio.cpp index fe453572bb1..646d6602056 100644 --- a/Engine/media/audio/audio.cpp +++ b/Engine/media/audio/audio.cpp @@ -416,7 +416,7 @@ ScriptAudioChannel* play_audio_clip_on_channel(int channel, ScriptAudioClip *cli soundfx->set_volume100(0); } - if (soundfx->play_from(fromOffset) == 0) + if (!soundfx->play_from(fromOffset)) { delete soundfx; debug_script_log("AudioClip.Play: failed to play sound file"); @@ -564,7 +564,7 @@ SOUNDCLIP *load_sound_and_play(ScriptAudioClip *aclip, bool repeat) SOUNDCLIP *soundfx = load_sound_clip(aclip, repeat); if (!soundfx) { return nullptr; } - if (soundfx->play() == 0) { + if (!soundfx->play()) { delete soundfx; return nullptr; } diff --git a/Engine/media/audio/audio_core.cpp b/Engine/media/audio/audio_core.cpp index 4e5b0626830..3861e0568c8 100644 --- a/Engine/media/audio/audio_core.cpp +++ b/Engine/media/audio/audio_core.cpp @@ -67,6 +67,9 @@ static void audio_core_entry(); void audio_core_init() { + if (g_acore.audio_core_thread_running) + return; // already running + /* InitAL opens a device and sets up a context using default attributes, making * the program ready to call OpenAL functions. */ @@ -105,6 +108,10 @@ void audio_core_init() void audio_core_shutdown() { + if (!g_acore.audio_core_thread_running) + return; // not running + + Debug::Printf(kDbgMsg_Info, "AudioCore: shutting down..."); g_acore.audio_core_thread_running = false; #if !defined(AGS_DISABLE_THREADS) if (g_acore.audio_core_thread.joinable()) @@ -127,6 +134,8 @@ void audio_core_shutdown() alcCloseDevice(g_acore.alcDevice); g_acore.alcDevice = nullptr; } + + Debug::Printf(kDbgMsg_Info, "AudioCore: shutdown"); } diff --git a/Engine/media/audio/audio_core.h b/Engine/media/audio/audio_core.h index 9e80949d16e..7617ccf25bd 100644 --- a/Engine/media/audio/audio_core.h +++ b/Engine/media/audio/audio_core.h @@ -17,57 +17,18 @@ //============================================================================= #ifndef __AGS_EE_MEDIA__AUDIOCORE_H #define __AGS_EE_MEDIA__AUDIOCORE_H -#include #include -#include #include #include "media/audio/audiodefines.h" #include "media/audio/audioplayer.h" #include "util/string.h" +#include "util/threading.h" -namespace AGS -{ -namespace Engine -{ - // AudioPlayerLock wraps a AudioPlayer pointer guarded by a mutex lock. // Unlocks the mutex on destruction (e.g. when going out of scope). -class AudioPlayerLock -{ -public: - AudioPlayerLock(AudioPlayer *player, std::unique_lock &&ulk, std::condition_variable *cv) - : _player(player) - , _ulock(std::move(ulk)) - , _cv(cv) - { - } - - AudioPlayerLock(AudioPlayerLock &&lock) - : _player(lock._player) - , _ulock(std::move(lock._ulock)) - , _cv(lock._cv) - { - lock._cv = nullptr; - } - - ~AudioPlayerLock() - { - if (_cv) - _cv->notify_all(); - } - - const AudioPlayer *operator ->() const { return _player; } - AudioPlayer *operator ->() { return _player; } - -private: - AudioPlayer *_player = nullptr; - std::unique_lock _ulock; - std::condition_variable *_cv = nullptr; -}; - -} // namespace Engine -} // namespace AGS +typedef AGS::Engine::LockedObjectPtr + AudioPlayerLock; // Initializes audio core system; // starts polling on a background thread. @@ -82,18 +43,18 @@ void audio_core_set_master_volume(float newvol); // Audio slot controls: slots are abstract holders for a playback. // -// Initializes playback on a free playback slot (reuses spare one or allocates new if there's none). +// Initializes playback on a free playback slot. // Data array must contain full wave data to play. -int audio_core_slot_init(std::shared_ptr> &data, const AGS::Common::String &extension_hint, bool repeat); +int audio_core_slot_init(std::shared_ptr> &data, const AGS::Common::String &ext_hint, bool repeat); // Initializes playback streaming -int audio_core_slot_init(std::unique_ptr in, const AGS::Common::String &extension_hint, bool repeat); +int audio_core_slot_init(std::unique_ptr in, const AGS::Common::String &ext_hint, bool repeat); // Returns a AudioPlayer from the given slot, wrapped in a auto-locking struct. -AGS::Engine::AudioPlayerLock audio_core_get_player(int slot_handle); +AudioPlayerLock audio_core_get_player(int slot_handle); // Stop and release the audio player at the given slot void audio_core_slot_stop(int slot_handle); #if defined(AGS_DISABLE_THREADS) -// polls the audio core if we have no threads, polled in Engine/ac/timer.cpp +// Polls the audio core if we have no threads, polled in WaitForNextFrame() void audio_core_entry_poll(); #endif diff --git a/Engine/media/audio/soundclip.cpp b/Engine/media/audio/soundclip.cpp index 14dbcd6af7a..98d509a850e 100644 --- a/Engine/media/audio/soundclip.cpp +++ b/Engine/media/audio/soundclip.cpp @@ -49,7 +49,7 @@ SOUNDCLIP::~SOUNDCLIP() audio_core_slot_stop(slot_); } -int SOUNDCLIP::play() +bool SOUNDCLIP::play() { if (!is_ready()) return false; diff --git a/Engine/media/audio/soundclip.h b/Engine/media/audio/soundclip.h index 9622dee55c2..0d7eb57b436 100644 --- a/Engine/media/audio/soundclip.h +++ b/Engine/media/audio/soundclip.h @@ -57,7 +57,7 @@ class SOUNDCLIP final int xSource, ySource; int maximumPossibleDistanceAway; - int play(); + bool play(); void pause(); void resume(); // Seeks to the position, where pos units depend on the audio type @@ -71,7 +71,7 @@ class SOUNDCLIP final // Returns if the clip is still playing, otherwise it's finished bool update(); - inline int play_from(int position) + inline bool play_from(int position) { seek(position); return play(); diff --git a/Engine/media/video/theora_player.h b/Engine/media/video/theora_player.h index be9e3184e04..c7b45628b70 100644 --- a/Engine/media/video/theora_player.h +++ b/Engine/media/video/theora_player.h @@ -14,6 +14,11 @@ // // Theora (OGV) video player implementation. // +// TODO: +// - support random Seek (APEG only provides reset/rewind atm). +// - for the long term - consider replacing APEG with a contemporary and +// feature-complete library. +// //============================================================================= #ifndef __AGS_EE_MEDIA__THEORAPLAYER_H #define __AGS_EE_MEDIA__THEORAPLAYER_H diff --git a/Engine/media/video/video.cpp b/Engine/media/video/video.cpp index 0c89f400eb2..da766475121 100644 --- a/Engine/media/video/video.cpp +++ b/Engine/media/video/video.cpp @@ -14,25 +14,27 @@ #include "media/video/video.h" #ifndef AGS_NO_VIDEO_PLAYER -#include -#include -#include #include "core/assetmanager.h" #include "ac/draw.h" +#include "ac/dynamicsprite.h" +#include "ac/game.h" #include "ac/game.h" #include "ac/gamesetupstruct.h" #include "ac/gamestate.h" #include "ac/global_audio.h" #include "ac/joystick.h" +#include "ac/spritecache.h" #include "ac/sys_events.h" +#include "ac/dynobj/dynobj_manager.h" +#include "ac/dynobj/scriptvideoplayer.h" #include "debug/debug_log.h" #include "gfx/graphicsdriver.h" #include "main/game_run.h" +#include "media/video/video_core.h" #include "media/audio/audio.h" -#include "media/video/videoplayer.h" -#include "media/video/flic_player.h" -#include "media/video/theora_player.h" +#include "media/video/video_core.h" #include "util/memory_compat.h" +#include "util/path.h" using namespace AGS::Common; using namespace AGS::Engine; @@ -55,7 +57,7 @@ namespace Engine class BlockingVideoPlayer : public GameState { public: - BlockingVideoPlayer(std::unique_ptr player, + BlockingVideoPlayer(int player_id, int video_flags, int state_flags, VideoSkipType skip); ~BlockingVideoPlayer(); @@ -70,12 +72,9 @@ class BlockingVideoPlayer : public GameState void Resume() override; private: -#if !defined(AGS_DISABLE_THREADS) - static void PollVideo(BlockingVideoPlayer *self); -#endif void StopVideo(); - std::unique_ptr _player; + int _playerID = 0; const String _assetName; // for diagnostics int _videoFlags = 0; int _stateFlags = 0; @@ -83,14 +82,12 @@ class BlockingVideoPlayer : public GameState Rect _dstRect; float _oldFps = 0.f; VideoSkipType _skip = VideoSkipNone; - std::thread _videoThread; - std::mutex _videoMutex; PlaybackState _playbackState = PlayStateInvalid; }; -BlockingVideoPlayer::BlockingVideoPlayer(std::unique_ptr player, +BlockingVideoPlayer::BlockingVideoPlayer(int player_id, int video_flags, int state_flags, VideoSkipType skip) - : _player(std::move(player)) + : _playerID(player_id) , _videoFlags(video_flags) , _stateFlags(state_flags) , _skip(skip) @@ -104,7 +101,7 @@ BlockingVideoPlayer::~BlockingVideoPlayer() void BlockingVideoPlayer::Begin() { - assert(_player); + assert(_playerID >= 0); // Optionally stop the game audio if ((_stateFlags & kVideoState_StopGameAudio) != 0) @@ -112,11 +109,34 @@ void BlockingVideoPlayer::Begin() stop_all_sound_and_music(); } + // Clear the screen before starting playback + // TODO: needed for FLIC, but perhaps may be done differently + if ((_stateFlags & kVideoState_ClearScreen) != 0) + { + if (gfxDriver->UsesMemoryBackBuffer()) + { + gfxDriver->GetMemoryBackBuffer()->Clear(); + } + render_to_screen(); + } + + auto player = video_core_get_player(_playerID); + + // Optionally adjust game speed, but only if it's lower than video's FPS + auto video_fps = player->GetFramerate(); + auto game_fps = get_game_speed(); + _oldFps = game_fps; + if (((_stateFlags & kVideoState_SetGameFps) != 0) && + (game_fps < video_fps)) + { + set_game_speed(video_fps); + } + // Setup video if ((_videoFlags & kVideo_EnableVideo) != 0) { - const bool software_draw = gfxDriver->HasAcceleratedTransform(); - Size frame_sz = _player->GetFrameSize(); + const bool software_draw = !gfxDriver->HasAcceleratedTransform(); + Size frame_sz = player->GetFrameSize(); Rect dest = PlaceInRect(play.GetMainViewport(), RectWH(frame_sz), ((_stateFlags & kVideoState_Stretch) == 0) ? kPlaceCenter : kPlaceStretchProportional); // override the stretch option if necessary @@ -129,10 +149,10 @@ void BlockingVideoPlayer::Begin() // because texture-based ones can scale the texture themselves. if (software_draw && (frame_sz != dest.GetSize())) { - _player->SetTargetFrame(dest.GetSize()); + player->SetTargetFrame(dest.GetSize()); } - const int dst_depth = _player->GetTargetDepth(); + const int dst_depth = player->GetTargetDepth(); if (!software_draw || ((_stateFlags & kVideoState_Stretch) == 0)) { _videoDDB = gfxDriver->CreateDDB(frame_sz.Width, frame_sz.Height, dst_depth, true); @@ -144,32 +164,8 @@ void BlockingVideoPlayer::Begin() _dstRect = dest; } - // Clear the screen before starting playback - // TODO: needed for FLIC, but perhaps may be done differently - if ((_stateFlags & kVideoState_ClearScreen) != 0) - { - if (gfxDriver->UsesMemoryBackBuffer()) - { - gfxDriver->GetMemoryBackBuffer()->Clear(); - } - render_to_screen(); - } - - auto video_fps = _player->GetFramerate(); - auto game_fps = get_game_speed(); - _oldFps = game_fps; - if (((_stateFlags & kVideoState_SetGameFps) != 0) && - (game_fps < video_fps)) - { - set_game_speed(video_fps); - } - - _player->Play(); - _playbackState = _player->GetPlayState(); - -#if !defined(AGS_DISABLE_THREADS) - _videoThread = std::thread(BlockingVideoPlayer::PollVideo, this); -#endif + player->Play(); + _playbackState = player->GetPlayState(); } void BlockingVideoPlayer::End() @@ -202,22 +198,22 @@ void BlockingVideoPlayer::End() void BlockingVideoPlayer::Pause() { - assert(_player); - std::lock_guard lk(_videoMutex); - _player->Pause(); + assert(_playerID >= 0); + auto player = video_core_get_player(_playerID); + player->Pause(); } void BlockingVideoPlayer::Resume() { - assert(_player); - std::lock_guard lk(_videoMutex); - _player->Play(); + assert(_playerID >= 0); + auto player = video_core_get_player(_playerID); + player->Play(); } bool BlockingVideoPlayer::Run() { - assert(_player); - if (!_player) + assert(_playerID >= 0); + if (_playerID < 0) return false; // Loop until finished or skipped by player if (IsPlaybackDone(GetPlayState())) @@ -228,17 +224,12 @@ bool BlockingVideoPlayer::Run() if (video_check_user_input(_skip)) return false; -#if defined(AGS_DISABLE_THREADS) - if (!_player->Poll()) - return false; -#endif - - // update/render next frame std::unique_ptr frame; { - std::lock_guard lk(_videoMutex); - _playbackState = _player->GetPlayState(); - frame = _player->GetReadyFrame(); + auto player = video_core_get_player(_playerID); + // update/render next frame + _playbackState = player->GetPlayState(); + frame = player->GetReadyFrame(); } if ((_videoFlags & kVideo_EnableVideo) != 0) @@ -249,8 +240,8 @@ bool BlockingVideoPlayer::Run() _videoDDB->SetStretch(_dstRect.GetWidth(), _dstRect.GetHeight(), false); { - std::lock_guard lk(_videoMutex); - _player->ReleaseFrame(std::move(frame)); + auto player = video_core_get_player(_playerID); + player->ReleaseFrame(std::move(frame)); } } } @@ -276,41 +267,17 @@ void BlockingVideoPlayer::Draw() void BlockingVideoPlayer::StopVideo() { // Stop player and wait for the thread to stop - if (_player) + if (_playerID >= 0) { - std::lock_guard lk(_videoMutex); - _player->Stop(); + video_core_slot_stop(_playerID); + _playerID = -1; } -#if !defined(AGS_DISABLE_THREADS) - if (_videoThread.joinable()) - _videoThread.join(); -#endif if (_videoDDB) gfxDriver->DestroyDDB(_videoDDB); _videoDDB = nullptr; } -#if !defined(AGS_DISABLE_THREADS) -/* static */ void BlockingVideoPlayer::PollVideo(BlockingVideoPlayer *self) -{ - assert(self && self->_player.get()); - if (!self || !self->_player.get()) - return; - - bool do_run = true; - while (do_run) - { - { - std::lock_guard lk(self->_videoMutex); - self->_player->Poll(); - do_run = IsPlaybackReady(self->_player->GetPlayState()); - } - std::this_thread::sleep_for(std::chrono::milliseconds(8)); - } -} -#endif - std::unique_ptr gl_Video; } // namespace Engine @@ -318,7 +285,8 @@ std::unique_ptr gl_Video; //----------------------------------------------------------------------------- -// Blocking video API +// Legacy Blocking video API +//----------------------------------------------------------------------------- // Running the single video playback //----------------------------------------------------------------------------- // Checks input events, tells if the video should be skipped @@ -354,27 +322,27 @@ static bool video_check_user_input(VideoSkipType skip) return false; } -static HError video_single_run(std::unique_ptr video, const String &asset_name, +static HError video_single_run(const String &asset_name, int video_flags, int state_flags, VideoSkipType skip) { - assert(video); - if (!video) - return HError::None(); - std::unique_ptr video_stream(AssetMgr->OpenAsset(asset_name)); if (!video_stream) { return new Error(String::FromFormat("Failed to open file: %s", asset_name.GetCStr())); } - const int dst_depth = game.GetColorDepth(); - HError err = video->Open(std::move(video_stream), asset_name, video_flags, Size(), dst_depth); - if (!err) + VideoInitParams params; + params.Flags = static_cast(video_flags); + params.TargetColorDepth = game.GetColorDepth(); + + video_core_init(); + auto slot = video_core_slot_init(std::move(video_stream), asset_name, Path::GetFileExtension(asset_name), params); + if (slot < 0) { - return new Error(String::FromFormat("Failed to run video %s", asset_name.GetCStr()), err); + return new Error(String::FromFormat("Failed to run video: %s", asset_name.GetCStr())); } - gl_Video.reset(new BlockingVideoPlayer(std::move(video), video_flags, state_flags, skip)); + gl_Video.reset(new BlockingVideoPlayer(slot, video_flags, state_flags, skip)); gl_Video->Begin(); while (gl_Video->Run()); gl_Video->End(); @@ -394,12 +362,12 @@ HError play_flc_video(int numb, int video_flags, int state_flags, VideoSkipType return new Error(String::FromFormat("FLIC animation flic%d.flc nor flic%d.fli were found", numb, numb)); } - return video_single_run(std::make_unique(), flicname, video_flags, state_flags, skip); + return video_single_run(flicname, video_flags, state_flags, skip); } HError play_theora_video(const char *name, int video_flags, int state_flags, VideoSkipType skip) { - return video_single_run(std::make_unique(), name, video_flags, state_flags, skip); + return video_single_run(name, video_flags, state_flags, skip); } void video_single_pause() @@ -419,17 +387,314 @@ void video_single_stop() gl_Video.reset(); } +//----------------------------------------------------------------------------- +// Non-blocking video API +//----------------------------------------------------------------------------- + +VideoControl::VideoControl(int video_id, int sprite_id) + : _videoID(video_id) + , _spriteID(sprite_id) +{ + auto player = video_core_get_player(video_id); + _frameRate = player->GetFramerate(); + _durMs = player->GetDurationMs(); + _frameCount = _durMs * 1000.0 / _frameRate; +} + +VideoControl::~VideoControl() +{ + if (_scriptHandle >= 0) + { + ScriptVideoPlayer *sc_video = (ScriptVideoPlayer*)ccGetObjectAddressFromHandle(_scriptHandle); + if (sc_video) + { + sc_video->Invalidate(); + // FIXME: need to fix ManagedPool to avoid recursive Dispose of disposing object!! + //ccAttemptDisposeObject(_scriptHandle); + } + } +} + +void VideoControl::SetScriptHandle(int sc_handle) +{ + _scriptHandle = sc_handle; + // TODO: do we need to check & invalidate previous handle, if there was any? +} + +bool VideoControl::Play() +{ + if (!IsReady()) + return false; + _state = PlaybackState::PlayStatePlaying; + return true; +} + +void VideoControl::Pause() +{ + if (!IsReady()) + return; + auto player = video_core_get_player(_videoID); + player->Pause(); + _state = player->GetPlayState(); +} + +bool VideoControl::NextFrame() +{ + if (!IsReady()) + return false; + + std::unique_ptr new_frame; + // Lock video player, sync play state and retrieve a new frame + { + auto player = video_core_get_player(_videoID); + player->Pause(); + _state = player->GetPlayState(); + _posMs = player->GetPositionMs(); + _frameIndex = player->GetFrameIndex(); + new_frame = player->NextFrame(); + } + + if (!new_frame) + return false; + + // Apply a new frame, return old frame to the player + auto old_frame = SetNewFrame(std::move(new_frame)); + if (old_frame) + { + auto player = video_core_get_player(_videoID); + player->ReleaseFrame(std::move(old_frame)); + } + return true; +} + +uint32_t VideoControl::SeekFrame(uint32_t frame) +{ + if (!IsReady()) + return -1; + auto player = video_core_get_player(_videoID); + player->Pause(); + uint32_t new_pos = player->SeekFrame(frame); + _state = player->GetPlayState(); + _posMs = player->GetPositionMs(); + _frameIndex = player->GetFrameIndex(); + return new_pos; +} + +float VideoControl::SeekMs(float pos_ms) +{ + if (!IsReady()) + return -1.f; + auto player = video_core_get_player(_videoID); + player->Pause(); + float new_pos = player->Seek(pos_ms); + _state = player->GetPlayState(); + _posMs = player->GetPositionMs(); + _frameIndex = player->GetFrameIndex(); + return new_pos; +} + +std::unique_ptr VideoControl::SetNewFrame(std::unique_ptr new_frame) +{ + // FIXME: this is ugly to use different levels of sprite interface here, + // expand dynamic_sprite group of functions instead! + auto old_sprite = spriteset.RemoveSprite(_spriteID); + spriteset.SetSprite(_spriteID, std::move(new_frame), SPF_DYNAMICALLOC | SPF_OBJECTOWNED); + game_sprite_updated(_spriteID, false); + return old_sprite; +} + +bool VideoControl::Update() +{ + if (!IsReady()) + return false; + + std::unique_ptr new_frame; + // Lock video player, sync play state and retrieve a new frame + { + auto player = video_core_get_player(_videoID); + // Get current video state + PlaybackState core_state = player->GetPlayState(); + _posMs = player->GetPositionMs(); + _frameIndex = player->GetFrameIndex(); + + // If video playback already stopped on its own, then exit update early + if (IsPlaybackDone(core_state)) + { + _state = core_state; + return false; + } + + // Apply new parameters, do this early in case we start playback + if (_paramsChanged) + { + auto vol_f = static_cast(_volume) / 255.0f; + if (vol_f < 0.0f) { vol_f = 0.0f; } + if (vol_f > 1.0f) { vol_f = 1.0f; } + + auto speed_f = _speed; + if (speed_f <= 0.0) { speed_f = 1.0f; } + + player->SetVolume(vol_f); + player->SetSpeed(speed_f); + _paramsChanged = false; + } + + // Apply new playback state + if (_state != core_state) + { + switch (_state) + { + case PlaybackState::PlayStatePlaying: + player->Play(); + _state = player->GetPlayState(); + break; + default: /* do nothing */ + break; + } + } + + if (_state != PlaybackState::PlayStatePlaying) + return false; + + // Try get new video frame + new_frame = player->GetReadyFrame(); + } + + // Apply a new frame, return old frame to the player + if (new_frame) + { + auto old_frame = SetNewFrame(std::move(new_frame)); + if (old_frame) + { + auto player = video_core_get_player(_videoID); + player->ReleaseFrame(std::move(old_frame)); + } + } + return true; +} + + +// map video ID (matching video core handle) to VideoControl object +std::unordered_map> gl_VideoObjects; + +HError open_video(const char *name, int video_flags, int &video_id) +{ + std::unique_ptr video_stream(AssetMgr->OpenAsset(name)); + if (!video_stream) + return new Error(String::FromFormat("Failed to open file: %s", name)); + + if (gl_VideoObjects.empty()) + video_core_init(); // start the thread + + VideoInitParams params; + params.Flags = static_cast(video_flags); + params.TargetColorDepth = game.GetColorDepth(); + int video_slot = video_core_slot_init(std::move(video_stream), name, Path::GetFileExtension(name), params); + if (video_slot < 0) // FIXME: return errors from slot_init? + return new Error(String::FromFormat("Failed to initialize video player for %s", name)); + + // Allocate a sprite slot for this video player's frames; + // note that the very first frame could be a dummy frame created as a placeholder + std::unique_ptr first_frame; + { + auto player = video_core_get_player(video_slot); + first_frame = player->GetEmptyFrame(); + } + int sprite_slot = add_dynamic_sprite(std::move(first_frame)); + if (sprite_slot <= 0) + return new Error("No free sprite slot to render video to"); + + auto video_ctrl = std::make_unique(video_slot, sprite_slot); + gl_VideoObjects[video_slot] = std::move(video_ctrl); + + // NOTE: we do not start playback right away, + // but do so during the synchronization step (see sync_video_playback). + + video_id = video_slot; + return HError::None(); +} + +VideoControl *get_video_control(int video_id) +{ + auto it = gl_VideoObjects.find(video_id); + if (it == gl_VideoObjects.end()) + return nullptr; + return it->second.get(); +} + +void video_stop(int video_id) +{ + auto it = gl_VideoObjects.find(video_id); + if (it == gl_VideoObjects.end()) + return; // wrong index + + int video_slot = it->first; + video_core_slot_stop(video_slot); + free_dynamic_sprite(it->second->GetSpriteID()); + gl_VideoObjects.erase(video_slot); + + if (gl_VideoObjects.empty()) + video_core_shutdown(); // stop the thread to avoid redundant processing +} + +void sync_video_playback() +{ + for (auto &obj : gl_VideoObjects) + { + VideoControl *video_ctrl = obj.second.get(); + video_ctrl->Update(); + } +} + +void update_video_system_on_game_loop() +{ + sync_video_playback(); +} + +//----------------------------------------------------------------------------- + void video_shutdown() { video_single_stop(); + video_core_shutdown(); + gl_VideoObjects.clear(); } #else -HError play_theora_video(const char *name, int video_flags, int state_flags, AGS::Engine::VideoSkipType skip) { return HError::None(); } -HError play_flc_video(int numb, int video_flags, int state_flags, AGS::Engine::VideoSkipType skip) { return HError::None(); } -void video_pause() {} -void video_resume() {} -void video_shutdown() {} +//----------------------------------------------------------------------------- +// Stubs for the video support +//----------------------------------------------------------------------------- + +using namespace AGS::Common; + +HError play_theora_video(const char *, int, int, AGS::Engine::VideoSkipType) +{ + return new Error("Video playback is not supported in this engine build."); +} +HError play_flc_video(int, int, int, AGS::Engine::VideoSkipType) +{ + return new Error("Video playback is not supported in this engine build."); +} +void video_single_pause() {} +void video_single_resume() {} +void video_single_stop() {} + +HError open_video(const char *, int, int &) +{ + return new Error("Video playback is not supported in this engine build."); +} +VideoControl *get_video_control(int) { return nullptr; } +void video_stop(int) { } +void sync_video_playback() { } +void update_video_system_on_game_loop() { } +void video_shutdown() { } + +void VideoControl::SetScriptHandle(int) {} +bool VideoControl::Play() { return false; } +void VideoControl::Pause() {} +bool VideoControl::NextFrame() { return false; } +uint32_t VideoControl::SeekFrame(uint32_t) { return -1; } +float VideoControl::SeekMs(float) { return -1.f; } #endif diff --git a/Engine/media/video/video.h b/Engine/media/video/video.h index 4ef6cc4e11b..ef48c82cda3 100644 --- a/Engine/media/video/video.h +++ b/Engine/media/video/video.h @@ -12,13 +12,51 @@ // //============================================================================= // -// Game-blocking video interface. +// Video playback logical frontend. Serves as a control layer between game +// script and working video thread(s). Synchronizes logical and real playback +// states once during each game update: this is done to guarantee that the +// video state is fixed within a script callback. +// +// Videos play in their own FPS rate by default, which typically may be +// different from the game's, but the frame that the game is displaying +// on screen is updated in game's FPS rate. This also means that if a video +// has higher FPS than the game's, then there will be visible frame skips. +// If video's FPS is lower, then the same video frame will simply be displayed +// for more than 1 game's frame (not a bad thing). +// +// There are two kinds of playback controls below: blocking and non-blocking. +// +// Blocking video is a legacy interface left for backwards compatibility. +// It completely stops game updates for the duration of video (optionally with +// exception of game's audio track). No game elements are drawn while it plays +// (neither GUI, nor even a mouse cursor). +// +// Non-blocking video interface is a new default, which supports full playback +// control, and lets assign a video frame to any game object on screen. +// Game script is responsible for anything that happens during playback. +// +//----------------------------------------------------------------------------- +// +// TODO: +// - in the engine support tagging sprites as "opaque" so that the renderer +// would know to use simpler texture update method (video frames do not +// have transparency in them... or can they?.. this may depend on info +// retrieved from the video file too). +// +// TODO: POTENTIAL ALTERNATIVES (for consideration) +// - current implementation of a non-blocking video provides a sprite +// as a raw bitmap that may be updated to a texture. This lets to assign +// video frame on any object. But a downside is the increased complexity, +// which may make optimizing for performance somewhat more difficult. +// An alternate implementation could instead render video frames on a +// specialized game object, e.g. VideoOverlay, a subclass of Overlay. // //============================================================================= #ifndef __AGS_EE_MEDIA__VIDEO_H #define __AGS_EE_MEDIA__VIDEO_H -#include "media/video/videoplayer.h" +#include "gfx/bitmap.h" +#include "media/audio/audiodefines.h" #include "util/geometry.h" #include "util/string.h" #include "util/error.h" @@ -54,7 +92,7 @@ enum VideoSkipType } // namespace AGS -// Blocking video API +// Legacy Blocking video API // // Start a blocking OGV playback AGS::Common::HError play_theora_video(const char *name, int video_flags, int state_flags, AGS::Engine::VideoSkipType skip); @@ -67,6 +105,94 @@ void video_single_resume(); // Stop current blocking video playback and dispose all video resource void video_single_stop(); +// Non-blocking video API +// +class VideoControl +{ +public: + VideoControl(int video_id, int sprite_id); + ~VideoControl(); + + // Gets if the video is valid (playing or ready to play) + bool IsReady() const + { + return IsPlaybackReady(_state); + } + + int GetVideoID() const { return _videoID; } + int GetSpriteID() const { return _spriteID; } + int GetScriptHandle() const { return _scriptHandle; } + + void SetScriptHandle(int sc_handle); + + int GetFrame() const { return _frameIndex; } + int GetFrameCount() const { return _frameCount; } + float GetFrameRate() const { return _frameRate; } + float GetDurationMs() const { return _durMs; } + float GetPositionMs() const { return _posMs; } + bool GetLooping() const { return _looping; } + PlaybackState GetState() const { return _state; } + float GetSpeed() const { return _speed; } + int GetVolume() const { return _volume; } + + void SetLooping(bool looping) + { + _looping = looping; + _paramsChanged = true; + } + + void SetSpeed(float speed) + { + _speed = speed; + _paramsChanged = true; + } + + void SetVolume(int vol) + { + _volume = vol; + _paramsChanged = true; + } + + bool Play(); + void Pause(); + bool NextFrame(); + uint32_t SeekFrame(uint32_t frame); + float SeekMs(float pos_ms); + + // Synchronize VideoControl with the video playback subsystem; + // - start scheduled playback; + // - apply all accumulated sound parameters; + // - read and save current position; + // Returns if the clip is still playing, otherwise it's finished + bool Update(); + +private: + std::unique_ptr SetNewFrame(std::unique_ptr frame); + + const int _videoID; + const int _spriteID; + int _scriptHandle = -1; + float _frameRate = 0.f; + float _durMs = 0.f; + uint32_t _frameCount = 0; + int _volume = 100; + float _speed = 1.f; + bool _looping = false; + PlaybackState _state = PlayStateInitial; + uint32_t _frameIndex = 0; + float _posMs = 0.f; + bool _paramsChanged = false; +}; + +// open_video starts video and returns a VideoControl object +// associated with it. +AGS::Common::HError open_video(const char *name, int video_flags, int &video_id); +VideoControl *get_video_control(int video_id); +void video_stop(int video_id); +// syncs logical video objects with the video core state +void sync_video_playback(); +// update non-blocking videos +void update_video_system_on_game_loop(); // Stop all videos and video thread void video_shutdown(); diff --git a/Engine/media/video/video_core.cpp b/Engine/media/video/video_core.cpp new file mode 100644 index 00000000000..80a1f7f0b4b --- /dev/null +++ b/Engine/media/video/video_core.cpp @@ -0,0 +1,197 @@ +//============================================================================= +// +// Adventure Game Studio (AGS) +// +// Copyright (C) 1999-2011 Chris Jones and 2011-2024 various contributors +// The full list of copyright holders can be found in the Copyright.txt +// file, which is part of this source code distribution. +// +// The AGS source code is provided under the Artistic License 2.0. +// A copy of this license can be found in the file License.txt and at +// https://opensource.org/license/artistic-2-0/ +// +//============================================================================= +#include "media/video/video_core.h" +#include +#include +#include +#include +#include "debug/out.h" +#include "media/video/videoplayer.h" +#include "media/video/flic_player.h" +#include "media/video/theora_player.h" + +using namespace AGS::Common; +using namespace AGS::Engine; + +#ifndef AGS_NO_VIDEO_PLAYER + +// Global audio core state and resources +static struct +{ + // Audio thread: polls sound decoders, feeds OpenAL sources + std::thread video_core_thread; + bool video_core_thread_running = false; + + // Slot id counter + int nextId = 0; + + // One mutex to lock them all... any operation on individual players + // is done under this only mutex, which means that they are currently + // polled one by one, any action like pause/resume is also synced. + std::mutex poll_mutex_m; + std::condition_variable poll_cv; + std::unordered_map> slots_; +} g_vcore; + +static void video_core_entry(); + +// ------------------------------------------------------------------------------------------------- +// INIT / SHUTDOWN +// ------------------------------------------------------------------------------------------------- + +void video_core_init() +{ + if (g_vcore.video_core_thread_running) + return; // already running + + g_vcore.video_core_thread_running = true; +#if !defined(AGS_DISABLE_THREADS) + g_vcore.video_core_thread = std::thread(video_core_entry); +#endif + Debug::Printf(kDbgMsg_Info, "VideoCore: init thread"); +} + +void video_core_shutdown() +{ + if (!g_vcore.video_core_thread_running) + return; // not running + + Debug::Printf(kDbgMsg_Info, "VideoCore: shutting down..."); + g_vcore.video_core_thread_running = false; +#if !defined(AGS_DISABLE_THREADS) + if (g_vcore.video_core_thread.joinable()) + g_vcore.video_core_thread.join(); +#endif + + // dispose all the active slots + g_vcore.slots_.clear(); + Debug::Printf(kDbgMsg_Info, "VideoCore: shutdown"); +} + + +// ------------------------------------------------------------------------------------------------- +// SLOTS +// ------------------------------------------------------------------------------------------------- + +static int avail_slot_id() +{ + return g_vcore.nextId++; +} + +static int video_core_slot_init(std::unique_ptr player) +{ + auto handle = avail_slot_id(); + std::lock_guard lk(g_vcore.poll_mutex_m); + g_vcore.slots_[handle] = std::move(player); + g_vcore.poll_cv.notify_all(); + return handle; +} + +static std::unique_ptr create_video_player(const AGS::Common::String &ext_hint) +{ + std::unique_ptr player; + // Table of video format detection + if (ext_hint.CompareNoCase("flc") == 0 || + ext_hint.CompareNoCase("fli") == 0) + player.reset(new FlicPlayer()); + else if (ext_hint.CompareNoCase("ogv") == 0) + player.reset(new TheoraPlayer()); + else + return nullptr; // not supported + return player; +} + +int video_core_slot_init(std::unique_ptr in, + const String &name, const AGS::Common::String &ext_hint, const VideoInitParams ¶ms) +{ + auto player = create_video_player(ext_hint); + if (!player) + return -1; + if (!player->Open(std::move(in), name, + params.Flags, params.TargetSize, params.TargetColorDepth, params.FPS)) + return -1; + return video_core_slot_init(std::move(player)); +} + +VideoPlayerLock video_core_get_player(int slot_handle) +{ + std::unique_lock ulk(g_vcore.poll_mutex_m); + auto it = g_vcore.slots_.find(slot_handle); + if (it == g_vcore.slots_.end()) + return VideoPlayerLock(nullptr, std::move(ulk), &g_vcore.poll_cv); + return VideoPlayerLock(it->second.get(), std::move(ulk), &g_vcore.poll_cv); +} + +void video_core_slot_stop(int slot_handle) +{ + std::lock_guard lk(g_vcore.poll_mutex_m); + g_vcore.slots_[slot_handle]->Stop(); + g_vcore.slots_.erase(slot_handle); + g_vcore.poll_cv.notify_all(); +} + +// ------------------------------------------------------------------------------------------------- +// VIDEO PROCESSING +// ------------------------------------------------------------------------------------------------- + +void video_core_entry_poll() +{ + for (auto &entry : g_vcore.slots_) { + auto &slot = entry.second; + + try { + slot->Poll(); + } catch (const std::exception& e) { + Debug::Printf(kDbgMsg_Error, "VideoCore poll exception: %s", e.what()); + } + } +} + +#if !defined(AGS_DISABLE_THREADS) +static void video_core_entry() +{ + std::unique_lock lk(g_vcore.poll_mutex_m); + + while (g_vcore.video_core_thread_running) { + + video_core_entry_poll(); + + g_vcore.poll_cv.wait_for(lk, std::chrono::milliseconds(8)); + } +} +#endif // !AGS_DISABLE_THREADS + +#else // AGS_NO_VIDEO_PLAYER + +void video_core_init(/*config*/) +{ + Debug::Printf(kDbgMsg_Warn, "VideoCore: video playback is not supported in this engine build."); +} +void video_core_shutdown() { } +int video_core_slot_init(std::unique_ptr, + const AGS::Common::String &, const AGS::Common::String &, const VideoInitParams &) +{ + return -1; +} +VideoPlayerLock video_core_get_player(int) +{ + throw std::runtime_error("Video playback is not supported in this engine build."); +} +void video_core_slot_stop(int) {} + +#if defined(AGS_DISABLE_THREADS) +void video_core_entry_poll() {} +#endif + +#endif // !AGS_NO_VIDEO_PLAYER diff --git a/Engine/media/video/video_core.h b/Engine/media/video/video_core.h new file mode 100644 index 00000000000..783e72c67a9 --- /dev/null +++ b/Engine/media/video/video_core.h @@ -0,0 +1,67 @@ +//============================================================================= +// +// Adventure Game Studio (AGS) +// +// Copyright (C) 1999-2011 Chris Jones and 2011-2024 various contributors +// The full list of copyright holders can be found in the Copyright.txt +// file, which is part of this source code distribution. +// +// The AGS source code is provided under the Artistic License 2.0. +// A copy of this license can be found in the file License.txt and at +// https://opensource.org/license/artistic-2-0/ +// +//============================================================================= +// +// Video Core: an video backend interface. +// +// TODO: +// - multiple working threads. +// - separate thread(s) for buffering, so that it continues even while +// the videoplayer is being synchronized with the game logic. +// +//============================================================================= +#ifndef __AGS_EE_MEDIA__VIDEOCORE_H +#define __AGS_EE_MEDIA__VIDEOCORE_H +#include +#include "media/video/videoplayer.h" +#include "util/string.h" +#include "util/threading.h" + +// AudioPlayerLock wraps a AudioPlayer pointer guarded by a mutex lock. +// Unlocks the mutex on destruction (e.g. when going out of scope). +typedef AGS::Engine::LockedObjectPtr + VideoPlayerLock; + +// Initializes video core system; +// starts polling on a background thread. +void video_core_init(/*config*/); +// Shut downs video core system; +// stops any associated threads. +void video_core_shutdown(); + +struct VideoInitParams +{ + AGS::Engine::VideoFlags Flags = AGS::Engine::kVideo_None; + Size TargetSize; + int TargetColorDepth = 0; + float FPS = 0.f; +}; + +// Video slot controls: slots are abstract holders for a playback. +// +// Initializes video playback on a free playback slot, uses provided +// stream for data streaming. +// TODO: return HError on error? +int video_core_slot_init(std::unique_ptr in, + const AGS::Common::String &name, const AGS::Common::String &ext_hint, const VideoInitParams ¶ms); +// Returns a VideoPlayer from the given slot, wrapped in a auto-locking struct. +VideoPlayerLock video_core_get_player(int slot_handle); +// Stop and release the video player at the given slot +void video_core_slot_stop(int slot_handle); + +#if defined(AGS_DISABLE_THREADS) +// Polls the video core if we have no threads, polled in WaitForNextFrame() +void video_core_entry_poll(); +#endif + +#endif // __AGS_EE_MEDIA__VIDEOCORE_H diff --git a/Engine/media/video/videoplayer.cpp b/Engine/media/video/videoplayer.cpp index 36a3e0a026f..b4f398a4aa4 100644 --- a/Engine/media/video/videoplayer.cpp +++ b/Engine/media/video/videoplayer.cpp @@ -14,6 +14,7 @@ #ifndef AGS_NO_VIDEO_PLAYER #include "media/video/videoplayer.h" #include "debug/out.h" +#include "util/memory_compat.h" namespace AGS { @@ -233,13 +234,18 @@ void VideoPlayer::SetVolume(float volume) _audioOut->SetVolume(volume); } -std::unique_ptr VideoPlayer::GetReadyFrame() +std::unique_ptr VideoPlayer::GetReadyFrame() { if (_framesPlayed > _wantFrameIndex) return nullptr; return NextFrameFromQueue(); } +std::unique_ptr VideoPlayer::GetEmptyFrame() +{ + return GetPooledFrame(); +} + void VideoPlayer::ReleaseFrame(std::unique_ptr frame) { _videoFramePool.push(std::move(frame)); @@ -323,16 +329,7 @@ void VideoPlayer::BufferVideo() return; // Get one frame from the pool, if present, otherwise allocate a new one - std::unique_ptr target_frame; - if (_videoFramePool.empty()) - { - target_frame.reset(new Bitmap(_targetSize.Width, _targetSize.Height, _targetDepth)); - } - else - { - target_frame = std::move(_videoFramePool.top()); - _videoFramePool.pop(); - } + std::unique_ptr target_frame = GetPooledFrame(); // Try to retrieve one video frame from decoder const bool must_conv = (_targetSize != _frameSize || _targetDepth != _frameDepth @@ -391,6 +388,20 @@ void VideoPlayer::UpdateTime() _framesPlayed);/**/ } +std::unique_ptr VideoPlayer::GetPooledFrame() +{ + if (_videoFramePool.empty()) + { + return std::make_unique(_targetSize.Width, _targetSize.Height, _targetDepth); + } + else + { + auto frame = std::move(_videoFramePool.top()); + _videoFramePool.pop(); + return frame; + } +} + std::unique_ptr VideoPlayer::NextFrameFromQueue() { if (_videoFrameQueue.empty()) diff --git a/Engine/media/video/videoplayer.h b/Engine/media/video/videoplayer.h index 0d5bd244c1b..001026fcb01 100644 --- a/Engine/media/video/videoplayer.h +++ b/Engine/media/video/videoplayer.h @@ -22,11 +22,21 @@ // - allow skip frames if late (add to settings); // - a video-audio sync mechanism; perhaps rely on audio, // because it's more time-sensitive in human perception; -// drop video frames if video is lagging, but this also has to -// be done in decoder to avoid converting vframe to a bitmap. +// drop video frames if video is lagging. But this also may have to +// be done in decoder to avoid converting vframe to a bitmap (yuv->rgb). // - other options: slow down playback speed until video-audio // relation stabilizes. // +// TODO: POTENTIAL OPTIMIZATIONS +// - create and buffer textures along the bitmaps. +// - perhaps skip bitmap and decode onto texture right away, +// that would require modifying the video decoding lib (or using other). +// but then there also has to be a reverse conversion made in case +// someone would like to use the frame for raw drawing. +// - expose buffering in video player's interface, and guard buffer queues +// with mutexes. This would allow to keep buffering constantly even +// during next ready frame retrieval by the engine. +// //============================================================================= #ifndef __AGS_EE_MEDIA__VIDEOPLAYER_H #define __AGS_EE_MEDIA__VIDEOPLAYER_H @@ -48,6 +58,7 @@ namespace Engine enum VideoFlags { + kVideo_None = 0, kVideo_EnableVideo = 0x0001, kVideo_EnableAudio = 0x0002, kVideo_Loop = 0x0004, @@ -116,6 +127,8 @@ class VideoPlayer // Retrieve the currently prepared video frame std::unique_ptr GetReadyFrame(); + // Retrieve a dummy clear frame, may be used as a temp placeholder + std::unique_ptr GetEmptyFrame(); // Tell VideoPlayer that this frame is not used anymore, and may be recycled // TODO: redo this part later, by introducing some kind of a RAII lock wrapper. void ReleaseFrame(std::unique_ptr frame); @@ -164,6 +177,8 @@ class VideoPlayer void BufferAudio(); // Update playback timing void UpdateTime(); + // Retrieve a frame from the pool, or create a new one + std::unique_ptr GetPooledFrame(); // Retrieve first available frame from queue, // advance output frame counter std::unique_ptr NextFrameFromQueue(); diff --git a/Engine/script/exports.cpp b/Engine/script/exports.cpp index 7b523168cf7..c5d3c01fb31 100644 --- a/Engine/script/exports.cpp +++ b/Engine/script/exports.cpp @@ -55,6 +55,7 @@ extern void RegisterSystemAPI(); extern void RegisterTextBoxAPI(); extern void RegisterViewFrameAPI(); extern void RegisterViewportAPI(); +extern void RegisterVideoAPI(); extern void RegisterWalkareaAPI(); extern void RegisterWalkbehindAPI(); @@ -99,6 +100,7 @@ void setup_script_exports(ScriptAPIVersion base_api, ScriptAPIVersion compat_api RegisterTextBoxAPI(); RegisterViewFrameAPI(); RegisterViewportAPI(); + RegisterVideoAPI(); RegisterWalkareaAPI(); RegisterWalkbehindAPI(); diff --git a/Engine/script/script_api.h b/Engine/script/script_api.h index 9b5c590f722..d53a8ee7974 100644 --- a/Engine/script/script_api.h +++ b/Engine/script/script_api.h @@ -382,6 +382,11 @@ inline const char *ScriptVSprintf(char *buffer, size_t buf_length, const char *f RET_CLASS* ret_obj = (RET_CLASS*)FUNCTION((P1CLASS*)params[0].Ptr, params[1].IValue); \ return RuntimeScriptValue().SetScriptObject(ret_obj, ret_obj) +#define API_SCALL_OBJAUTO_POBJ_PINT2(RET_CLASS, FUNCTION, P1CLASS) \ + ASSERT_PARAM_COUNT(FUNCTION, 3); \ + RET_CLASS* ret_obj = (RET_CLASS*)FUNCTION((P1CLASS*)params[0].Ptr, params[1].IValue, params[2].IValue); \ + return RuntimeScriptValue().SetScriptObject(ret_obj, ret_obj) + #define API_SCALL_OBJAUTO_POBJ_PINT4(RET_CLASS, FUNCTION, P1CLASS) \ ASSERT_PARAM_COUNT(FUNCTION, 5); \ RET_CLASS* ret_obj = FUNCTION((P1CLASS*)params[0].Ptr, params[1].IValue, params[2].IValue, params[3].IValue, params[4].IValue); \ diff --git a/Engine/util/threading.h b/Engine/util/threading.h new file mode 100644 index 00000000000..1a4b443821e --- /dev/null +++ b/Engine/util/threading.h @@ -0,0 +1,71 @@ +//============================================================================= +// +// Adventure Game Studio (AGS) +// +// Copyright (C) 1999-2011 Chris Jones and 2011-2024 various contributors +// The full list of copyright holders can be found in the Copyright.txt +// file, which is part of this source code distribution. +// +// The AGS source code is provided under the Artistic License 2.0. +// A copy of this license can be found in the file License.txt and at +// https://opensource.org/license/artistic-2-0/ +// +//============================================================================= +// +// Threading utilities +// +//============================================================================= +#ifndef __AGS_EE_UTIL__THREADING_H +#define __AGS_EE_UTIL__THREADING_H +#include +#include + +namespace AGS +{ +namespace Engine +{ + +// LockedObjectPtr wraps an object pointer guarded by a mutex lock. +// Unlocks the mutex on destruction (e.g. when going out of scope). +// Optionally accepts a condition variable, sends notification on destruction. +template +class LockedObjectPtr +{ +public: + LockedObjectPtr(T *object, std::unique_lock &&ulk, std::condition_variable *cv) + : _object(object) + , _ulock(std::move(ulk)) + , _cv(cv) + { + } + + LockedObjectPtr(LockedObjectPtr &&lock) + : _object(lock._object) + , _ulock(std::move(lock._ulock)) + , _cv(lock._cv) + { + lock._cv = nullptr; + } + + ~LockedObjectPtr() + { + if (_cv) + _cv->notify_all(); + } + + operator bool() const { return _object != nullptr; } + const T *operator ->() const { return _object; } + T *operator ->() { return _object; } + const T &operator *() const { return *_object; } + T &operator *() { return *_object; } + +private: + T *_object = nullptr; + std::unique_lock _ulock; + std::condition_variable *_cv = nullptr; +}; + +} // namespace Engine +} // namespace AGS + +#endif // __AGS_EE_UTIL__THREADING_H diff --git a/Solutions/Engine.App/Engine.App.vcxproj b/Solutions/Engine.App/Engine.App.vcxproj index ea881c64df7..0b0814b6df3 100644 --- a/Solutions/Engine.App/Engine.App.vcxproj +++ b/Solutions/Engine.App/Engine.App.vcxproj @@ -502,6 +502,7 @@ + @@ -563,6 +564,7 @@ + @@ -635,6 +637,7 @@ + @@ -746,6 +749,7 @@ + @@ -888,6 +892,7 @@ + @@ -914,6 +919,7 @@ + diff --git a/Solutions/Engine.App/Engine.App.vcxproj.filters b/Solutions/Engine.App/Engine.App.vcxproj.filters index b42dd1809c8..93af23d7e59 100644 --- a/Solutions/Engine.App/Engine.App.vcxproj.filters +++ b/Solutions/Engine.App/Engine.App.vcxproj.filters @@ -774,6 +774,15 @@ Source Files\media\video + + Source Files\media\video + + + Source Files\ac + + + Source Files\ac\dynobj + @@ -1550,6 +1559,15 @@ Header Files\media\video + + Header Files\media\video + + + Header Files\util + + + Header Files\ac\dynobj +