Skip to content

Commit

Permalink
Engine: implement video Looping, Rewind and NextFrame
Browse files Browse the repository at this point in the history
  • Loading branch information
ivan-mogilko committed Feb 28, 2024
1 parent afe77f0 commit 7bc3587
Show file tree
Hide file tree
Showing 10 changed files with 206 additions and 53 deletions.
8 changes: 4 additions & 4 deletions Editor/AGS.Editor/Resources/agsdefns.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2674,10 +2674,10 @@ builtin managed struct VideoPlayer {
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.
import void SeekFrame(int frame);
/// Changes playback to continue from the specified position in milliseconds.
import void SeekMs(int position);
/// 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();
Expand Down
23 changes: 16 additions & 7 deletions Engine/ac/video_script.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -66,17 +66,26 @@ void VideoPlayer_Pause(ScriptVideoPlayer *sc_video)

void VideoPlayer_NextFrame(ScriptVideoPlayer *sc_video)
{
// TODO
if (sc_video->GetID() < 0)
return;
VideoControl *video_ctrl = get_video_control(sc_video->GetID());
video_ctrl->NextFrame();
}

void VideoPlayer_SeekFrame(ScriptVideoPlayer *sc_video, int frame)
int VideoPlayer_SeekFrame(ScriptVideoPlayer *sc_video, int frame)
{
// TODO
if (sc_video->GetID() < 0)
return 0;
VideoControl *video_ctrl = get_video_control(sc_video->GetID());
return video_ctrl->SeekFrame(frame);
}

void VideoPlayer_SeekMs(ScriptVideoPlayer *sc_video, int pos)
int VideoPlayer_SeekMs(ScriptVideoPlayer *sc_video, int pos)
{
// TODO
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)
Expand Down Expand Up @@ -215,12 +224,12 @@ RuntimeScriptValue Sc_VideoPlayer_NextFrame(void *self, const RuntimeScriptValue

RuntimeScriptValue Sc_VideoPlayer_SeekFrame(void *self, const RuntimeScriptValue *params, int32_t param_count)
{
API_OBJCALL_VOID_PINT(ScriptVideoPlayer, VideoPlayer_SeekFrame);
API_OBJCALL_INT_PINT(ScriptVideoPlayer, VideoPlayer_SeekFrame);
}

RuntimeScriptValue Sc_VideoPlayer_SeekMs(void *self, const RuntimeScriptValue *params, int32_t param_count)
{
API_OBJCALL_VOID_PINT(ScriptVideoPlayer, VideoPlayer_SeekMs);
API_OBJCALL_INT_PINT(ScriptVideoPlayer, VideoPlayer_SeekMs);
}

RuntimeScriptValue Sc_VideoPlayer_Stop(void *self, const RuntimeScriptValue *params, int32_t param_count)
Expand Down
36 changes: 31 additions & 5 deletions Engine/media/video/theora_player.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,27 @@ void apeg_stream_skip(int bytes, void *ptr)
}
//

HError TheoraPlayer::OpenImpl(std::unique_ptr<Common::Stream> data_stream,
HError TheoraPlayer::OpenImpl(std::unique_ptr<Stream> data_stream,
const String &name, int &flags, int target_depth)
{
// Init APEG
HError err = OpenAPEGStream(data_stream.get(), name, flags, target_depth);
if (!err)
return err;

_name = name;
_dataStream = std::move(data_stream);
return HError::None();
}

HError TheoraPlayer::OpenAPEGStream(Stream *data_stream, const Common::String &name, int flags, int target_depth)
{
if (_apegStream)
{
apeg_close_stream(_apegStream);
_apegStream = nullptr;
}

// NOTE: following settings affect only next apeg_open_stream* or
// apeg_reset_stream.
apeg_set_stream_reader(apeg_stream_init, apeg_stream_read, apeg_stream_skip);
Expand All @@ -63,7 +81,7 @@ HError TheoraPlayer::OpenImpl(std::unique_ptr<Common::Stream> data_stream,
apeg_disable_length_detection(TRUE);
apeg_ignore_audio((flags & kVideo_EnableAudio) == 0);

APEG_STREAM* apeg_stream = apeg_open_stream_ex(data_stream.get());
APEG_STREAM* apeg_stream = apeg_open_stream_ex(data_stream);
if (!apeg_stream)
{
return new Error(String::FromFormat("Failed to open theora video '%s'; could be an invalid or unsupported format", name.GetCStr()));
Expand All @@ -75,9 +93,8 @@ HError TheoraPlayer::OpenImpl(std::unique_ptr<Common::Stream> data_stream,
}

_apegStream = apeg_stream;

// Init APEG
_dataStream = std::move(data_stream);
_usedFlags = flags;
_usedDepth = target_depth;
_frameDepth = target_depth;
_frameRate = _apegStream->frame_rate;
_frameSize = Size(video_w, video_h);
Expand Down Expand Up @@ -109,6 +126,15 @@ void TheoraPlayer::CloseImpl()
_apegStream = nullptr;
}

bool TheoraPlayer::RewindImpl()
{
if (apeg_reset_stream(_apegStream) != APEG_OK)
{
OpenAPEGStream(_dataStream.get(), _name, _usedFlags, _usedDepth);
}
return _apegStream != nullptr;
}

bool TheoraPlayer::NextVideoFrame(Bitmap *dst)
{
assert(_apegStream);
Expand Down
6 changes: 6 additions & 0 deletions Engine/media/video/theora_player.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,18 @@ class TheoraPlayer : public VideoPlayer
Common::HError OpenImpl(std::unique_ptr<Common::Stream> data_stream,
const String &name, int &flags, int target_depth) override;
void CloseImpl() override;
bool RewindImpl() override;
// Retrieves next video frame, implementation-specific
bool NextVideoFrame(Common::Bitmap *dst) override;
// Retrieves next audio frame, implementation-specific
SoundBuffer NextAudioFrame() override;

Common::HError OpenAPEGStream(Stream *data_stream, const String &name, int flags, int target_depth);

String _name;
std::unique_ptr<Stream> _dataStream;
int _usedFlags = 0;
int _usedDepth = 0;
APEG_STREAM *_apegStream = nullptr;
// Optional wrapper around original buffer frame (in case we want to extract a portion of it)
std::unique_ptr<Common::Bitmap> _theoraFullFrame;
Expand Down
62 changes: 50 additions & 12 deletions Engine/media/video/video.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,54 @@ void VideoControl::Pause()
_state = video_core_slot_pause(_videoID);
}

bool VideoControl::NextFrame()
{
if (!IsReady())
return false;
video_core_slot_pause(_videoID);
if (!video_core_slot_next_frame(_videoID))
return false;
_state = video_core_slot_get_play_state(_videoID, &_posMs);
_frameIndex; // FIXME
TryAcquireFrame();
return true;
}

uint32_t VideoControl::SeekFrame(uint32_t frame)
{
if (!IsReady())
return -1;
video_core_slot_pause(_videoID);
bool res = video_core_slot_seek(_videoID, 0.f, frame);
video_core_slot_get_play_state(_videoID, &_posMs, &_frameIndex);
return res ? _frameIndex : UINT32_MAX;
}

float VideoControl::SeekMs(float pos_ms)
{
if (!IsReady())
return -1.f;
video_core_slot_pause(_videoID);
bool res = video_core_slot_seek(_videoID, pos_ms);
video_core_slot_get_play_state(_videoID, &_posMs, &_frameIndex);
return res ? _posMs : -1.f;
}

void VideoControl::TryAcquireFrame()
{
auto new_frame = video_core_slot_acquire_vframe(_videoID);
if (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);
game_sprite_updated(_spriteID, false);
// Give old frame back to video
video_core_slot_release_vframe(_videoID, std::move(old_sprite));
}
}

bool VideoControl::Update()
{
if (!IsReady())
Expand All @@ -369,7 +417,7 @@ bool VideoControl::Update()
auto speed_f = _speed;
if (speed_f <= 0.0) { speed_f = 1.0f; }

video_core_slot_configure(_videoID, vol_f, speed_f);
video_core_slot_configure(_videoID, vol_f, speed_f, _looping);
_paramsChanged = false;
}

Expand All @@ -390,17 +438,7 @@ bool VideoControl::Update()
return false;

// Try get new video frame
auto new_frame = video_core_slot_acquire_vframe(_videoID);
if (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);
game_sprite_updated(_spriteID, false);
// Give old frame back to video
video_core_slot_release_vframe(_videoID, std::move(old_sprite));
}
TryAcquireFrame();
return true;
}

Expand Down
5 changes: 5 additions & 0 deletions Engine/media/video/video.h
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ class VideoControl

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;
Expand All @@ -127,6 +130,8 @@ class VideoControl
bool Update();

private:
void TryAcquireFrame();

const int _videoID;
const int _spriteID;
int _scriptHandle = -1;
Expand Down
23 changes: 17 additions & 6 deletions Engine/media/video/video_core.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -150,16 +150,24 @@ void video_core_slot_stop(int slot_handle)
g_vcore.poll_cv.notify_all();
}

void video_core_slot_seek_ms(int slot_handle, float pos_ms)
bool video_core_slot_seek(int slot_handle, float pos_ms, uint32_t frame)
{
std::lock_guard<std::mutex> lk(g_vcore.poll_mutex_m);
g_vcore.slots_[slot_handle]->Seek(pos_ms);
bool res;
if (frame != UINT32_MAX)
res = g_vcore.slots_[slot_handle]->SeekFrame(pos_ms);
else
res = g_vcore.slots_[slot_handle]->Seek(pos_ms);
g_vcore.poll_cv.notify_all();
return res;
}

void video_core_slot_next_frame(int slot_handle)
bool video_core_slot_next_frame(int slot_handle)
{
// TODO
std::lock_guard<std::mutex> lk(g_vcore.poll_mutex_m);
bool res = g_vcore.slots_[slot_handle]->NextFrame();
g_vcore.poll_cv.notify_all();
return res;
}

std::unique_ptr<AGS::Common::Bitmap> video_core_slot_acquire_vframe(int slot_handle)
Expand All @@ -184,12 +192,13 @@ void video_core_slot_release_vframe(int slot_handle, std::unique_ptr<AGS::Common
// FIXME: move this to OpenAlSource, because this seems to be related to its implementation
const auto GlobalGainScaling = 0.7f; // TODO: find out why 0.7f is here?

void video_core_slot_configure(int slot_handle, float volume, float speed)
void video_core_slot_configure(int slot_handle, float volume, float speed, bool loop)
{
std::lock_guard<std::mutex> lk(g_vcore.poll_mutex_m);
auto &player = g_vcore.slots_[slot_handle];
player->SetVolume(volume * GlobalGainScaling);
player->SetSpeed(speed);
player->SetLooping(loop);
g_vcore.poll_cv.notify_all();
}

Expand All @@ -213,12 +222,14 @@ float video_core_slot_get_duration(int slot_handle)
return dur;
}

PlaybackState video_core_slot_get_play_state(int slot_handle, float *pos_ms)
PlaybackState video_core_slot_get_play_state(int slot_handle, float *pos_ms, uint32_t *frame_pos)
{
std::lock_guard<std::mutex> lk(g_vcore.poll_mutex_m);
auto state = g_vcore.slots_[slot_handle]->GetPlayState();
if (pos_ms)
*pos_ms = g_vcore.slots_[slot_handle]->GetPositionMs();
if (frame_pos)
*frame_pos = g_vcore.slots_[slot_handle]->GetFrameIndex();
g_vcore.poll_cv.notify_all();
return state;
}
Expand Down
12 changes: 6 additions & 6 deletions Engine/media/video/video_core.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,11 @@ PlaybackState video_core_slot_play(int slot_handle);
PlaybackState video_core_slot_pause(int slot_handle);
// Stop playback on a slot, disposes sound data, frees a slot
void video_core_slot_stop(int slot_handle);
// Seek on a slot, new position in milliseconds
void video_core_slot_seek_ms(int slot_handle, float pos_ms);
// Seek on a slot, new position in milliseconds or in frames
bool video_core_slot_seek(int slot_handle, float pos_ms, uint32_t frame = UINT32_MAX);
// Advanced a single video frame on a slot;
// this works even if slot is paused.
void video_core_slot_next_frame(int slot_handle);
// this pauses the video
bool video_core_slot_next_frame(int slot_handle);

// Locks and returns last prepared video frame;
// while locked this bitmap cannot be modified.
Expand All @@ -80,10 +80,10 @@ void video_core_entry_poll();
// Video core config
// Sets up single playback parameters
// TODO: consider a struct for passing params instead, perhaps separate video and audio params
void video_core_slot_configure(int slot_handle, float volume, float speed);
void video_core_slot_configure(int slot_handle, float volume, float speed, bool loop);

// Returns current playback state, optionally fills in position
PlaybackState video_core_slot_get_play_state(int slot_handle, float *pos_ms = nullptr);
PlaybackState video_core_slot_get_play_state(int slot_handle, float *pos_ms = nullptr, uint32_t *frame_pos = nullptr);
// Returns video position in milliseconds
float video_core_slot_get_pos_ms(int slot_handle);
// Returns video duration in milliseconds
Expand Down
Loading

0 comments on commit 7bc3587

Please sign in to comment.