mirror of
https://github.com/OpenSpace/OpenSpace.git
synced 2026-04-27 14:29:37 -05:00
Move video stretching into video player
This commit is contained in:
@@ -39,6 +39,11 @@
|
||||
|
||||
namespace openspace {
|
||||
|
||||
enum class PlaybackMode {
|
||||
MapToSimulationTime = 0,
|
||||
RealTimeLoop
|
||||
};
|
||||
|
||||
class VideoPlayer : public properties::PropertyOwner {
|
||||
public:
|
||||
VideoPlayer(const ghoul::Dictionary& dictionary);
|
||||
@@ -59,6 +64,7 @@ public:
|
||||
double videoDuration() const;
|
||||
double fps() const;
|
||||
double currentPlaybackTime() const;
|
||||
PlaybackMode playbackMode() const;
|
||||
|
||||
void reset();
|
||||
void destroy();
|
||||
@@ -71,6 +77,20 @@ public:
|
||||
properties::TriggerProperty _reset;
|
||||
properties::BoolProperty _playAudio;
|
||||
private:
|
||||
|
||||
PlaybackMode _playbackMode = PlaybackMode::RealTimeLoop; // Default is to loop
|
||||
|
||||
// Map to simulation time functions
|
||||
double correctVideoPlaybackTime() const;
|
||||
bool isWithingStartEndTime() const;
|
||||
void updateFrameDuration();
|
||||
void syncToSimulationTime();
|
||||
|
||||
// Video stretching: map to simulation time animation mode
|
||||
double _startJ200Time = 0.0;
|
||||
double _endJ200Time = 0.0;
|
||||
double _timeAtLastRender = 0.0;
|
||||
double _frameDuration = 0.0;
|
||||
|
||||
// libmpv property keys
|
||||
enum class LibmpvPropertyKey : uint64_t {
|
||||
|
||||
@@ -56,38 +56,16 @@ public:
|
||||
Tile tile(const TileIndex& tileIndex) override final;
|
||||
Tile::Status tileStatus(const TileIndex& tileIndex) override final;
|
||||
TileDepthTransform depthTransform() override final;
|
||||
void syncToSimulationTime();
|
||||
|
||||
static documentation::Documentation Documentation();
|
||||
|
||||
private:
|
||||
enum class PlaybackMode {
|
||||
MapToSimulationTime = 0,
|
||||
RealTimeLoop
|
||||
};
|
||||
|
||||
// Map to simulation time functions
|
||||
double correctVideoPlaybackTime() const;
|
||||
bool isWithingStartEndTime() const;
|
||||
void updateFrameDuration();
|
||||
|
||||
void internalInitialize() override final;
|
||||
void internalDeinitialize() override final;
|
||||
|
||||
PlaybackMode _playbackMode = PlaybackMode::RealTimeLoop; // Default is to loop
|
||||
|
||||
// Video stretching: map to simulation time animation mode
|
||||
double _startJ200Time = 0.0;
|
||||
double _endJ200Time = 0.0;
|
||||
double _timeAtLastRender = 0.0;
|
||||
double _frameDuration = 0.0;
|
||||
double _fps = 24.0;
|
||||
double _videoDuration = 1.0;
|
||||
|
||||
// Tile handling
|
||||
std::map<TileIndex::TileHashKey, Tile> _tileCache; // Cache for rendering 1 frame
|
||||
bool _tileIsReady = false;
|
||||
double _seekThreshold = 1.0; // Threshold to ensure we seek to a different time
|
||||
|
||||
VideoPlayer _videoPlayer;
|
||||
};
|
||||
|
||||
@@ -59,11 +59,15 @@ RenderableVideoSphere::RenderableVideoSphere(const ghoul::Dictionary& dictionary
|
||||
: RenderableSphere(dictionary)
|
||||
, _videoPlayer(dictionary)
|
||||
{
|
||||
addProperty(_videoPlayer._play);
|
||||
addProperty(_videoPlayer._pause);
|
||||
addProperty(_videoPlayer._goToStart);
|
||||
addProperty(_videoPlayer._reset);
|
||||
addProperty(_videoPlayer._playAudio);
|
||||
|
||||
if (_videoPlayer.playbackMode() == PlaybackMode::RealTimeLoop) {
|
||||
// Video interaction. Only valid for real time looping
|
||||
addProperty(_videoPlayer._play);
|
||||
addProperty(_videoPlayer._pause);
|
||||
addProperty(_videoPlayer._goToStart);
|
||||
}
|
||||
}
|
||||
|
||||
void RenderableVideoSphere::bindTexture() {
|
||||
|
||||
@@ -66,11 +66,15 @@ ScreenSpaceVideo::ScreenSpaceVideo(const ghoul::Dictionary& dictionary)
|
||||
identifier = makeUniqueIdentifier(identifier);
|
||||
setIdentifier(identifier);
|
||||
|
||||
addProperty(_videoPlayer._play);
|
||||
addProperty(_videoPlayer._pause);
|
||||
addProperty(_videoPlayer._goToStart);
|
||||
addProperty(_videoPlayer._reset);
|
||||
addProperty(_videoPlayer._playAudio);
|
||||
|
||||
if (_videoPlayer.playbackMode() == PlaybackMode::RealTimeLoop) {
|
||||
// Video interaction. Only valid for real time looping
|
||||
addProperty(_videoPlayer._play);
|
||||
addProperty(_videoPlayer._pause);
|
||||
addProperty(_videoPlayer._goToStart);
|
||||
}
|
||||
}
|
||||
|
||||
void ScreenSpaceVideo::update() {
|
||||
|
||||
@@ -72,12 +72,48 @@ namespace {
|
||||
"Play audio"
|
||||
};
|
||||
|
||||
constexpr openspace::properties::Property::PropertyInfo StartTimeInfo = {
|
||||
"StartTime",
|
||||
"Start Time",
|
||||
"The date and time that the video should start in the format "
|
||||
"'YYYY MM DD hh:mm:ss'."
|
||||
};
|
||||
|
||||
constexpr openspace::properties::Property::PropertyInfo EndTimeInfo = {
|
||||
"EndTime",
|
||||
"End Time",
|
||||
"The date and time that the video should end in the format "
|
||||
"'YYYY MM DD hh:mm:ss'."
|
||||
};
|
||||
|
||||
constexpr openspace::properties::Property::PropertyInfo PlaybackModeInfo = {
|
||||
"PlaybackMode",
|
||||
"Playback Mode",
|
||||
"Determines the way the video should be played. The start and end time of the "
|
||||
"video can be set, or the video can be played as a loop in real time."
|
||||
};
|
||||
|
||||
struct [[codegen::Dictionary(VideoPlayer)]] Parameters {
|
||||
// [[codegen::verbatim(VideoInfo.description)]]
|
||||
std::string video;
|
||||
|
||||
// [[codegen::verbatim(AudioInfo.description)]]
|
||||
std::optional<bool> playAudio;
|
||||
|
||||
// [[codegen::verbatim(StartTimeInfo.description)]]
|
||||
std::optional<std::string> startTime [[codegen::datetime()]];
|
||||
|
||||
// [[codegen::verbatim(EndTimeInfo.description)]]
|
||||
std::optional<std::string> endTime [[codegen::datetime()]];
|
||||
|
||||
enum class PlaybackMode {
|
||||
MapToSimulationTime = 0,
|
||||
RealTimeLoop
|
||||
};
|
||||
|
||||
// The mode of how the video should be played back.
|
||||
// Default is video is played back according to the set start and end times.
|
||||
std::optional<PlaybackMode> playbackMode;
|
||||
};
|
||||
#include "videoplayer_codegen.cpp"
|
||||
} // namespace
|
||||
@@ -173,6 +209,41 @@ VideoPlayer::VideoPlayer(const ghoul::Dictionary& dictionary)
|
||||
_goToStart.onChange([this]() { goToStart(); });
|
||||
_reset.onChange([this]() { reset(); });
|
||||
_playAudio.onChange([this]() { toggleMute(); });
|
||||
|
||||
if (p.playbackMode.has_value()) {
|
||||
switch (*p.playbackMode) {
|
||||
case Parameters::PlaybackMode::RealTimeLoop:
|
||||
_playbackMode = PlaybackMode::RealTimeLoop;
|
||||
break;
|
||||
case Parameters::PlaybackMode::MapToSimulationTime:
|
||||
_playbackMode = PlaybackMode::MapToSimulationTime;
|
||||
break;
|
||||
default:
|
||||
LERROR("Missing playback mode in VideoTileProvider");
|
||||
throw ghoul::MissingCaseException();
|
||||
}
|
||||
}
|
||||
|
||||
if (_playbackMode == PlaybackMode::MapToSimulationTime) {
|
||||
if (!p.startTime.has_value() || !p.endTime.has_value()) {
|
||||
LERROR("Video tile layer tried to map to simulation time but lacked start or"
|
||||
" end time");
|
||||
return;
|
||||
}
|
||||
_startJ200Time = Time::convertTime(*p.startTime);
|
||||
_endJ200Time = Time::convertTime(*p.endTime);
|
||||
ghoul_assert(_endJ200Time > _startJ200Time, "Invalid times for video");
|
||||
|
||||
// Change the video time if OpenSpace time changes
|
||||
global::timeManager->addTimeJumpCallback([this]() {
|
||||
seekToTime(correctVideoPlaybackTime());
|
||||
});
|
||||
|
||||
// Ensure we are synchronized to OpenSpace time in presync step
|
||||
global::callback::preSync->emplace_back([this]() {
|
||||
syncToSimulationTime();
|
||||
});
|
||||
}
|
||||
|
||||
global::callback::postSyncPreDraw->emplace_back([this]() {
|
||||
if (_isDestroying) {
|
||||
@@ -556,6 +627,9 @@ void VideoPlayer::handleMpvProperties(mpv_event* event) {
|
||||
}
|
||||
|
||||
_videoDuration = *duration;
|
||||
if (_playbackMode == PlaybackMode::MapToSimulationTime) {
|
||||
updateFrameDuration();
|
||||
}
|
||||
|
||||
LINFO(fmt::format("Duration: {}", *duration));
|
||||
break;
|
||||
@@ -740,6 +814,9 @@ void VideoPlayer::handleMpvProperties(mpv_event* event) {
|
||||
break;
|
||||
}
|
||||
_fps = *fps;
|
||||
if (_playbackMode == PlaybackMode::MapToSimulationTime) {
|
||||
updateFrameDuration();
|
||||
}
|
||||
|
||||
LINFO(fmt::format("Detected fps: {}", *fps));
|
||||
_seekThreshold = 2.0 * (1.0 / _fps);
|
||||
@@ -811,6 +888,66 @@ double VideoPlayer::currentPlaybackTime() const {
|
||||
return _currentVideoTime;
|
||||
}
|
||||
|
||||
PlaybackMode VideoPlayer::playbackMode() const {
|
||||
return _playbackMode;
|
||||
}
|
||||
|
||||
|
||||
bool VideoPlayer::isWithingStartEndTime() const {
|
||||
const double now = global::timeManager->time().j2000Seconds();
|
||||
return now <= _endJ200Time && now >= _startJ200Time;
|
||||
}
|
||||
|
||||
void VideoPlayer::updateFrameDuration() {
|
||||
double openspaceVideoLength = (_endJ200Time - _startJ200Time) / _videoDuration;
|
||||
_frameDuration = (1.0 / _fps) * openspaceVideoLength;
|
||||
}
|
||||
|
||||
double VideoPlayer::correctVideoPlaybackTime() const {
|
||||
const double now = global::timeManager->time().j2000Seconds();
|
||||
double percentage = 0.0;
|
||||
if (now > _endJ200Time) {
|
||||
percentage = 1.0;
|
||||
}
|
||||
else if (now < _startJ200Time) {
|
||||
percentage = 0.0;
|
||||
}
|
||||
else {
|
||||
percentage = (now - _startJ200Time) / (_endJ200Time - _startJ200Time);
|
||||
}
|
||||
return percentage * videoDuration();
|
||||
}
|
||||
|
||||
void VideoPlayer::syncToSimulationTime() {
|
||||
if (_playbackMode == PlaybackMode::MapToSimulationTime) {
|
||||
// If we are in valid times, step frames accordingly
|
||||
if (isWithingStartEndTime()) {
|
||||
double now = global::timeManager->time().j2000Seconds();
|
||||
double deltaTime = now - _timeAtLastRender;
|
||||
if (deltaTime > _frameDuration) {
|
||||
// Stepping forwards
|
||||
stepFrameForward();
|
||||
_timeAtLastRender = now;
|
||||
}
|
||||
else if (deltaTime < -_frameDuration) {
|
||||
// Stepping backwards
|
||||
stepFrameBackward();
|
||||
_timeAtLastRender = now;
|
||||
}
|
||||
}
|
||||
else if (!_isPaused) {
|
||||
pause();
|
||||
}
|
||||
// Make sure we are at the correct time
|
||||
double time = correctVideoPlaybackTime();
|
||||
bool shouldSeek = abs(time - _currentVideoTime) > _seekThreshold;
|
||||
if (shouldSeek) {
|
||||
seekToTime(time);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void VideoPlayer::createFBO(int width, int height) {
|
||||
LINFO(fmt::format("Creating new FBO with width: {} and height: {}", width, height));
|
||||
|
||||
|
||||
@@ -38,42 +38,7 @@
|
||||
namespace {
|
||||
constexpr std::string_view _loggerCat = "VideoTileProvider";
|
||||
|
||||
constexpr openspace::properties::Property::PropertyInfo StartTimeInfo = {
|
||||
"StartTime",
|
||||
"Start Time",
|
||||
"The date and time that the video should start in the format "
|
||||
"'YYYY MM DD hh:mm:ss'."
|
||||
};
|
||||
|
||||
constexpr openspace::properties::Property::PropertyInfo EndTimeInfo = {
|
||||
"EndTime",
|
||||
"End Time",
|
||||
"The date and time that the video should end in the format "
|
||||
"'YYYY MM DD hh:mm:ss'."
|
||||
};
|
||||
|
||||
constexpr openspace::properties::Property::PropertyInfo PlaybackModeInfo = {
|
||||
"PlaybackMode",
|
||||
"Playback Mode",
|
||||
"Determines the way the video should be played. The start and end time of the "
|
||||
"video can be set, or the video can be played as a loop in real time."
|
||||
};
|
||||
|
||||
struct [[codegen::Dictionary(VideoTileProvider)]] Parameters {
|
||||
// [[codegen::verbatim(StartTimeInfo.description)]]
|
||||
std::optional<std::string> startTime [[codegen::datetime()]];
|
||||
|
||||
// [[codegen::verbatim(EndTimeInfo.description)]]
|
||||
std::optional<std::string> endTime [[codegen::datetime()]];
|
||||
|
||||
enum class PlaybackMode {
|
||||
MapToSimulationTime = 0,
|
||||
RealTimeLoop
|
||||
};
|
||||
|
||||
// The mode of how the video should be played back.
|
||||
// Default is video is played back according to the set start and end times.
|
||||
std::optional<PlaybackMode> playbackMode;
|
||||
|
||||
};
|
||||
#include "videotileprovider_codegen.cpp"
|
||||
@@ -99,45 +64,12 @@ VideoTileProvider::VideoTileProvider(const ghoul::Dictionary& dictionary)
|
||||
addProperty(_videoPlayer._reset);
|
||||
addProperty(_videoPlayer._playAudio);
|
||||
|
||||
if (p.playbackMode.has_value()) {
|
||||
switch (*p.playbackMode) {
|
||||
case Parameters::PlaybackMode::RealTimeLoop:
|
||||
_playbackMode = PlaybackMode::RealTimeLoop;
|
||||
break;
|
||||
case Parameters::PlaybackMode::MapToSimulationTime:
|
||||
_playbackMode = PlaybackMode::MapToSimulationTime;
|
||||
break;
|
||||
default:
|
||||
LERROR("Missing playback mode in VideoTileProvider");
|
||||
throw ghoul::MissingCaseException();
|
||||
}
|
||||
}
|
||||
|
||||
if (_playbackMode == PlaybackMode::RealTimeLoop) {
|
||||
if (_videoPlayer.playbackMode() == PlaybackMode::RealTimeLoop) {
|
||||
// Video interaction. Only valid for real time looping
|
||||
addProperty(_videoPlayer._play);
|
||||
addProperty(_videoPlayer._pause);
|
||||
addProperty(_videoPlayer._goToStart);
|
||||
}
|
||||
else if (_playbackMode == PlaybackMode::MapToSimulationTime) {
|
||||
if (!p.startTime.has_value() || !p.endTime.has_value()) {
|
||||
LERROR("Video tile layer tried to map to simulation time but lacked start or"
|
||||
" end time");
|
||||
return;
|
||||
}
|
||||
_startJ200Time = Time::convertTime(*p.startTime);
|
||||
_endJ200Time = Time::convertTime(*p.endTime);
|
||||
ghoul_assert(_endJ200Time > _startJ200Time, "Invalid times for video");
|
||||
|
||||
// Change the video time if OpenSpace time changes
|
||||
global::timeManager->addTimeJumpCallback([this]() {
|
||||
_videoPlayer.seekToTime(correctVideoPlaybackTime());
|
||||
});
|
||||
// Ensure we are synchronized to OpenSpace time in presync step
|
||||
global::callback::preSync->emplace_back([this]() {
|
||||
syncToSimulationTime();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Tile VideoTileProvider::tile(const TileIndex& tileIndex) {
|
||||
@@ -165,7 +97,6 @@ Tile VideoTileProvider::tile(const TileIndex& tileIndex) {
|
||||
Tile::Status::OK
|
||||
};
|
||||
}
|
||||
|
||||
return _tileCache[hash];
|
||||
}
|
||||
|
||||
@@ -208,75 +139,6 @@ ChunkTile VideoTileProvider::chunkTile(TileIndex tileIndex, int parents, int max
|
||||
return traverseTree(tileIndex, parents, maxParents, ascendToParent, uvTransform);
|
||||
}
|
||||
|
||||
bool VideoTileProvider::isWithingStartEndTime() const {
|
||||
const double now = global::timeManager->time().j2000Seconds();
|
||||
return now <= _endJ200Time && now >= _startJ200Time;
|
||||
}
|
||||
|
||||
void VideoTileProvider::updateFrameDuration() {
|
||||
double openspaceVideoLength = (_endJ200Time - _startJ200Time) / _videoDuration;
|
||||
_frameDuration = (1.0 / _fps) * openspaceVideoLength;
|
||||
}
|
||||
|
||||
double VideoTileProvider::correctVideoPlaybackTime() const {
|
||||
const double now = global::timeManager->time().j2000Seconds();
|
||||
double percentage = 0.0;
|
||||
if (now > _endJ200Time) {
|
||||
percentage = 1.0;
|
||||
}
|
||||
else if (now < _startJ200Time) {
|
||||
percentage = 0.0;
|
||||
}
|
||||
else {
|
||||
percentage = (now - _startJ200Time) / (_endJ200Time - _startJ200Time);
|
||||
}
|
||||
return percentage * _videoPlayer.videoDuration();
|
||||
}
|
||||
|
||||
void VideoTileProvider::syncToSimulationTime() {
|
||||
if (_playbackMode == PlaybackMode::MapToSimulationTime) {
|
||||
bool fpsChanged = isDifferent(_videoPlayer.fps(), _fps);
|
||||
bool durationChanged = isDifferent(_videoPlayer.videoDuration(), _videoDuration);
|
||||
if (fpsChanged) {
|
||||
_fps = _videoPlayer.fps();
|
||||
}
|
||||
if (durationChanged) {
|
||||
_videoDuration = _videoPlayer.videoDuration();
|
||||
}
|
||||
|
||||
if (fpsChanged || durationChanged) {
|
||||
updateFrameDuration();
|
||||
}
|
||||
if (!_videoPlayer.isPaused()) {
|
||||
//_videoPlayer.pause();
|
||||
}
|
||||
// If we are in valid times, step frames accordingly
|
||||
if (isWithingStartEndTime()) {
|
||||
double now = global::timeManager->time().j2000Seconds();
|
||||
double deltaTime = now - _timeAtLastRender;
|
||||
if (deltaTime > _frameDuration) {
|
||||
// Stepping forwards
|
||||
_videoPlayer.stepFrameForward();
|
||||
_timeAtLastRender = now;
|
||||
}
|
||||
else if (deltaTime < -_frameDuration) {
|
||||
// Stepping backwards
|
||||
_videoPlayer.stepFrameBackward();
|
||||
_timeAtLastRender = now;
|
||||
}
|
||||
}
|
||||
else if (!_videoPlayer.isPaused()) {
|
||||
_videoPlayer.pause();
|
||||
}
|
||||
// Make sure we are at the correct time
|
||||
double time = correctVideoPlaybackTime();
|
||||
bool shouldSeek = abs(time - _videoPlayer.currentPlaybackTime()) > _seekThreshold;
|
||||
if (shouldSeek) {
|
||||
_videoPlayer.seekToTime(time);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int VideoTileProvider::minLevel() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
local layer = {
|
||||
Identifier = "SosLoopTest",
|
||||
File = "C:/Users/ylvaselling/Documents/Work/Testmovies/chlorophyll_model_2048.mp4",
|
||||
Video = "C:/Users/ylvaselling/Documents/Work/Testmovies/chlorophyll_model_2048.mp4",
|
||||
Name = "Science On A Sphere Loop Video",
|
||||
Enabled = asset.enabled,
|
||||
Type = "VideoTileLayer",
|
||||
|
||||
Reference in New Issue
Block a user