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 18166ad7264..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 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/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 73dfa28a988..b307c73fb17 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/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/video.cpp b/Engine/media/video/video.cpp index d3e46db2d14..42d3953ec9e 100644 --- a/Engine/media/video/video.cpp +++ b/Engine/media/video/video.cpp @@ -16,15 +16,21 @@ #ifndef AGS_NO_VIDEO_PLAYER #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/video_core.h" #include "util/memory_compat.h" @@ -283,7 +289,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 @@ -384,10 +391,257 @@ 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; + auto player = video_core_get_player(_videoID); + player->Pause(); + auto new_frame = player->NextFrame(); + if (!new_frame) + return false; + _state = player->GetPlayState(); + _posMs = player->GetPositionMs(); + _frameIndex = player->GetFrameIndex(); + auto old_frame = SetNewFrame(std::move(new_frame)); + if (old_frame) + 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; + + 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 + auto new_frame = player->GetReadyFrame(); + if (new_frame) + { + auto old_frame = SetNewFrame(std::move(new_frame)); + if (old_frame) + 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 diff --git a/Engine/media/video/video.h b/Engine/media/video/video.h index 4ef6cc4e11b..3d0c06d8090 100644 --- a/Engine/media/video/video.h +++ b/Engine/media/video/video.h @@ -18,7 +18,8 @@ #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 +55,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 +68,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 index 11128e85403..17d05b01432 100644 --- a/Engine/media/video/video_core.cpp +++ b/Engine/media/video/video_core.cpp @@ -118,7 +118,8 @@ int video_core_slot_init(std::unique_ptr in, auto player = create_video_player(ext_hint); if (!player) return -1; - if (!player->Open(std::move(in), name, params.Flags, params.TargetSize, params.TargetColorDepth)) + 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)); } diff --git a/Engine/media/video/video_core.h b/Engine/media/video/video_core.h index e645d552492..c055407407a 100644 --- a/Engine/media/video/video_core.h +++ b/Engine/media/video/video_core.h @@ -39,7 +39,7 @@ struct VideoInitParams AGS::Engine::VideoFlags Flags = AGS::Engine::kVideo_None; Size TargetSize; int TargetColorDepth = 0; - int FPS = 0; + float FPS = 0.f; }; // Video slot controls: slots are abstract holders for a playback. 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 30cddcc9396..e848e32cce3 100644 --- a/Engine/media/video/videoplayer.h +++ b/Engine/media/video/videoplayer.h @@ -117,6 +117,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); @@ -165,6 +167,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/Solutions/Engine.App/Engine.App.vcxproj b/Solutions/Engine.App/Engine.App.vcxproj index 84c4a86ab27..0b0814b6df3 100644 --- a/Solutions/Engine.App/Engine.App.vcxproj +++ b/Solutions/Engine.App/Engine.App.vcxproj @@ -502,6 +502,7 @@ + @@ -563,6 +564,7 @@ + @@ -747,6 +749,7 @@ + diff --git a/Solutions/Engine.App/Engine.App.vcxproj.filters b/Solutions/Engine.App/Engine.App.vcxproj.filters index dd26201cfdd..93af23d7e59 100644 --- a/Solutions/Engine.App/Engine.App.vcxproj.filters +++ b/Solutions/Engine.App/Engine.App.vcxproj.filters @@ -777,6 +777,12 @@ Source Files\media\video + + Source Files\ac + + + Source Files\ac\dynobj + @@ -1559,6 +1565,9 @@ Header Files\util + + Header Files\ac\dynobj +