Timeline management in Player

Actions at key times (durations of all videos) to allow to adjust other videos duration (change speed of cut)
This commit is contained in:
Bruno
2021-06-14 23:42:20 +02:00
parent 99ea14fab0
commit 37445b8857
8 changed files with 338 additions and 60 deletions

View File

@@ -145,7 +145,10 @@ bool ImGuiToolkit::ButtonIcon(int i, int j, const char *tooltip)
ImVec2 uv1( uv0.x + 0.05, uv0.y + 0.05 );
ImGui::PushID( i*20 + j);
bool ret = ImGui::ImageButton((void*)(intptr_t)textureicons, ImVec2(ImGui::GetTextLineHeightWithSpacing(),ImGui::GetTextLineHeightWithSpacing()), uv0, uv1, 3);
ImGuiContext& g = *GImGui;
bool ret = ImGui::ImageButton((void*)(intptr_t)textureicons,
ImVec2(g.FontSize, g.FontSize),
uv0, uv1, g.Style.FramePadding.y);
ImGui::PopID();
if (tooltip != nullptr && ImGui::IsItemHovered())
@@ -353,6 +356,23 @@ bool ImGuiToolkit::ComboIcon (std::vector<std::pair<int, int> > icons, std::vect
return ret;
}
bool ImGuiToolkit::MenuItemIcon (int i, int j, const char* label, bool selected, bool enabled)
{
ImVec2 draw_pos = ImGui::GetCursorScreenPos();
// make some space
char text_buf[256];
ImFormatString(text_buf, IM_ARRAYSIZE(text_buf), " %s", label);
// draw menu item
bool ret = ImGui::MenuItem(text_buf, NULL, selected, enabled);
// draw icon
ImGui::SetCursorScreenPos(draw_pos);
Icon(i, j, enabled);
return ret;
}
void ImGuiToolkit::ShowIconsWindow(bool* p_open)
{

View File

@@ -17,11 +17,12 @@ namespace ImGuiToolkit
bool IconToggle (int i, int j, int i_toggle, int j_toggle, bool* toggle, const char *tooltips[] = nullptr);
void ShowIconsWindow(bool* p_open);
// icon buttons
// buttons and gui items with icon
bool ButtonIcon (int i, int j, const char* tooltip = nullptr);
bool ButtonIconToggle (int i, int j, int i_toggle, int j_toggle, bool* toggle);
bool ButtonIconMultistate (std::vector<std::pair<int, int> > icons, int* state);
bool ComboIcon (std::vector<std::pair<int, int> > icons, std::vector<std::string> labels, int* state);
bool MenuItemIcon (int i, int j, const char* label, bool selected = false, bool enabled = true);
// buttons
bool ButtonToggle (const char* label, bool* toggle);

View File

@@ -676,7 +676,7 @@ bool MediaPlayer::go_to(GstClockTime pos)
GstClockTime jumpPts = pos;
if (timeline_.gapAt(pos, gap)) {
if (timeline_.getGapAt(pos, gap)) {
// if in a gap, find closest seek target
if (gap.is_valid()) {
// jump in one or the other direction
@@ -922,7 +922,7 @@ void MediaPlayer::update()
else {
// manage timeline: test if position falls into a gap
TimeInterval gap;
if (position_ != GST_CLOCK_TIME_NONE && timeline_.gapAt(position_, gap)) {
if (position_ != GST_CLOCK_TIME_NONE && timeline_.getGapAt(position_, gap)) {
// if in a gap, seek to next section
if (gap.is_valid()) {
// jump in one or the other direction

View File

@@ -90,7 +90,7 @@ GstClockTime Timeline::next(GstClockTime time) const
GstClockTime next_time = time;
TimeInterval gap;
if (gapAt(time, gap) && gap.is_valid())
if (getGapAt(time, gap) && gap.is_valid())
next_time = gap.end;
return next_time;
@@ -100,7 +100,7 @@ GstClockTime Timeline::previous(GstClockTime time) const
{
GstClockTime prev_time = time;
TimeInterval gap;
if (gapAt(time, gap) && gap.is_valid())
if (getGapAt(time, gap) && gap.is_valid())
prev_time = gap.begin;
return prev_time;
@@ -125,7 +125,13 @@ void Timeline::refresh()
fillArrayFromGaps(gapsArray_, MAX_TIMELINE_ARRAY);
}
bool Timeline::gapAt(const GstClockTime t, TimeInterval &gap) const
bool Timeline::gapAt(const GstClockTime t) const
{
TimeIntervalSet::const_iterator g = std::find_if(gaps_.begin(), gaps_.end(), includesTime(t));
return ( g != gaps_.end() );
}
bool Timeline::getGapAt(const GstClockTime t, TimeInterval &gap) const
{
TimeIntervalSet::const_iterator g = std::find_if(gaps_.begin(), gaps_.end(), includesTime(t));
@@ -142,33 +148,77 @@ bool Timeline::addGap(GstClockTime begin, GstClockTime end)
return addGap( TimeInterval(begin, end) );
}
bool Timeline::cut(GstClockTime t)
bool Timeline::cut(GstClockTime t, bool left, bool join_extremity)
{
bool ret = false;
if (timing_.includes(t))
{
TimeIntervalSet::iterator g = std::find_if(gaps_.begin(), gaps_.end(), includesTime(t));
TimeIntervalSet::iterator gap = std::find_if(gaps_.begin(), gaps_.end(), includesTime(t));
// cut left part
if (left) {
// cut a gap
if ( g != gaps_.end() )
if ( gap != gaps_.end() )
{
GstClockTime b = g->begin;
gaps_.erase(g);
GstClockTime b = gap->begin;
gaps_.erase(gap);
ret = addGap(b, t);
}
// create a gap
else {
TimeIntervalSet::iterator previous = gaps_.end();
for (g = gaps_.begin(); g != gaps_.end(); previous = g++) {
auto previous = gaps_.end();
for (auto g = gaps_.begin(); g != gaps_.end(); previous = g++) {
if ( g->begin > t)
break;
}
if (join_extremity) {
//TODO
}
else {
if (previous == gaps_.end())
ret = addGap( TimeInterval(timing_.begin, t) );
else {
GstClockTime b = previous->begin;
gaps_.erase(previous);
ret = addGap(b, t);
ret = addGap( TimeInterval(b, t) );
}
}
}
}
// cut right part
else {
// cut a gap
if ( gap != gaps_.end() )
{
GstClockTime e = gap->end;
gaps_.erase(gap);
ret = addGap(t, e);
}
// create a gap
else {
auto suivant = gaps_.rend();
for (auto g = gaps_.rbegin(); g != gaps_.rend(); suivant = g++) {
if ( g->end < t)
break;
}
if (join_extremity) {
if (suivant != gaps_.rend()) {
for (auto g = gaps_.find(*suivant); g != gaps_.end(); ) {
g = gaps_.erase(g);
}
}
ret = addGap( TimeInterval(t, timing_.end) );
}
else {
if (suivant == gaps_.rend())
ret = addGap( TimeInterval(t, timing_.end) );
else {
GstClockTime e = suivant->end;
gaps_.erase( gaps_.find(*suivant));
ret = addGap( TimeInterval(t, e) );
}
}
}
}
}

View File

@@ -118,9 +118,11 @@ public:
void setGaps(const TimeIntervalSet &g);
bool addGap(TimeInterval s);
bool addGap(GstClockTime begin, GstClockTime end);
bool cut(GstClockTime t);
bool cut(GstClockTime t, bool left, bool join_extremity);
bool removeGaptAt(GstClockTime t);
bool gapAt(const GstClockTime t, TimeInterval &gap) const;
bool gapAt(const GstClockTime t) const;
bool getGapAt(const GstClockTime t, TimeInterval &gap) const;
// Manipulation of Fading
float fadingAt(const GstClockTime t) const;

View File

@@ -1986,6 +1986,7 @@ void ToolBox::Render()
SourceController::SourceController() : _min_width(0.f), _h_space(0.f), _v_space(0.f), _buttons_height(0.f),
_timeline_height(0.f), _scrollbar(0.f), _mediaplayer_height(0.f), _buttons_width(0.f),
active_label_(LABEL_AUTO_MEDIA_PLAYER), active_selection_(-1),
_selection_context_menu(false), _selection_mediaplayer(nullptr), _selection_target_slower(0), _selection_target_faster(0),
media_playing_mode_(false), slider_pressed_(false)
{
info_.setExtendedStringMode();
@@ -2275,18 +2276,24 @@ void SourceController::RenderSelection(size_t i)
// get max duration and max frame width
GstClockTime maxduration = 0;
std::list<guint64> durations;
float maxframewidth = 0.f;
for (auto source = selection_.begin(); source != selection_.end(); ++source) {
// collect durations of all media sources
MediaSource *ms = dynamic_cast<MediaSource *>(*source);
if (ms != nullptr) {
GstClockTime d = (static_cast<double>(ms->mediaplayer()->timeline()->sectionsDuration()) / ms->mediaplayer()->playSpeed());
if ( d > maxduration )
maxduration = d;
}
if (ms != nullptr)
durations.push_back(static_cast<guint64>(static_cast<double>(ms->mediaplayer()->timeline()->sectionsDuration()) / ms->mediaplayer()->playSpeed()));
// compute the displayed width of frames of this source, and keep the max to align afterwards
float w = 1.5f * _timeline_height * (*source)->frame()->aspectRatio();
if ( w > maxframewidth)
maxframewidth = w;
}
if (durations.size()>0) {
durations.sort();
durations.unique();
maxduration = durations.back();
}
// compute the ratio for timeline rendering : width (pixel) per time unit (ms)
const float w = rendersize.x -maxframewidth - 3.f * _h_space - _scrollbar;
const double width_ratio = static_cast<double>(w) / static_cast<double>(maxduration);
@@ -2294,6 +2301,7 @@ void SourceController::RenderSelection(size_t i)
// draw list in a scroll area
ImGui::BeginChild("##v_scroll2", rendersize, false);
{
// draw play time scale if a duration is set
if (maxduration > 0) {
ImGui::SetCursorPos(ImGui::GetCursorPos() + ImVec2( maxframewidth + _h_space, 0));
@@ -2311,7 +2319,9 @@ void SourceController::RenderSelection(size_t i)
UserInterface::manager().showSourceEditor(*source);
// text below thumbnail to show status
ImGuiToolkit::PushFont(ImGuiToolkit::FONT_MONO);
ImGui::Text("%s %s", SourcePlayIcon(*source), GstToolkit::time_to_string((*source)->playtime()).c_str() );
ImGui::PopFont();
// get media source
MediaSource *ms = dynamic_cast<MediaSource *>(*source);
@@ -2320,21 +2330,124 @@ void SourceController::RenderSelection(size_t i)
MediaPlayer *mp = ms->mediaplayer();
// start to draw timeline aligned at maximum frame width + horizontal space
ImGui::SetCursorPos(image_top + ImVec2( maxframewidth + _h_space, 0));
ImVec2 pos = image_top + ImVec2( maxframewidth + _h_space, 0);
ImGui::SetCursorPos(pos);
// draw the mediaplayer's timeline, with the indication of cursor position
// NB: use the same width/time ratio for all to ensure timing vertical correspondance
DrawTimeline("##timeline_mediaplayer", mp->timeline(), mp->position(), width_ratio / fabs(mp->playSpeed()), framesize.y);
ImGui::SetCursorPos(image_top + ImVec2( maxframewidth + _h_space, framesize.y + _v_space));
ImGui::Text("%s play time @ %.2f speed / %s (max duration)",
GstToolkit::time_to_string(mp->timeline()->sectionsDuration()).c_str(),
mp->playSpeed(), GstToolkit::time_to_string(maxduration).c_str());
// next icon buttons are small
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(3.f, 3.f));
// next buttons sub id
ImGui::PushID( static_cast<int>(mp->id()));
// display play speed
ImGui::SetCursorPos(pos + ImVec2( 0.f, framesize.y + _v_space));
ImGui::Text(UNICODE_MULTIPLY " %.2f", mp->playSpeed());
// if not 1x speed, offer to reset
if ( fabs( fabs(mp->playSpeed()) - 1.0 ) > EPSILON ) {
ImGui::SameLine(0,_h_space);
if (ImGuiToolkit::ButtonIcon(19, 15, "Reset speed"))
mp->setPlaySpeed( 1.0 );
}
// if more than one duration of media players, add buttons to adjust
if (durations.size() > 1)
{
for (auto d = durations.crbegin(); d != durations.crend(); ++d) {
ImGui::PushID( static_cast<int>(*d));
// calculate position of icons
double x = static_cast<double>(*d) * width_ratio;
ImGui::SetCursorPos(pos + ImVec2( static_cast<float>(x) - 2.f, framesize.y + _v_space) );
// depending on position relative to media play duration, offer corresponding action
double secdur = static_cast<double>(mp->timeline()->sectionsDuration());
guint64 playdur = static_cast<guint64>( secdur / fabs(mp->playSpeed()) );
// last icon in the timeline
if ( playdur == (*d) ) {
// not the minimum duration :
if (playdur > durations.front() ) {
// offer to speed up or slow down [<>]
if (playdur < durations.back() ) {
if ( ImGuiToolkit::ButtonIcon(0, 12, "Adjust duration") ) {
auto prev = d;
prev--;
_selection_target_slower = SIGN(mp->playSpeed()) * secdur / static_cast<double>(*prev);
auto next = d;
next++;
_selection_target_faster = SIGN(mp->playSpeed()) * secdur / static_cast<double>(*next);
_selection_mediaplayer = mp;
_selection_context_menu = true;
}
}
// offer to speed up [< ]
else if ( ImGuiToolkit::ButtonIcon(8, 12, "Adjust duration") ) {
auto next = d;
next++;
_selection_target_faster = SIGN(mp->playSpeed()) * secdur / static_cast<double>(*next);
_selection_target_slower = 0.0;
_selection_mediaplayer = mp;
_selection_context_menu = true;
}
}
// minimum duration : offer to slow down [ >]
else if ( ImGuiToolkit::ButtonIcon(9, 12, "Adjust duration") ) {
_selection_target_faster = 0.0;
auto prev = d;
prev--;
_selection_target_slower = SIGN(mp->playSpeed()) * secdur / static_cast<double>(*prev);
_selection_mediaplayer = mp;
_selection_context_menu = true;
}
}
// middle buttons : offer to cut at this position
else if ( playdur > (*d) ) {
char text_buf[256];
GstClockTime cutposition = (*d) * fabs(mp->playSpeed());
ImFormatString(text_buf, IM_ARRAYSIZE(text_buf), "Cut at %s",
GstToolkit::time_to_string(cutposition, GstToolkit::TIME_STRING_MINIMAL).c_str());
if ( ImGuiToolkit::ButtonIcon(9, 3, text_buf) ) {
if ( mp->timeline()->cut(cutposition, false, true) ) {
std::ostringstream info;
info << SystemToolkit::base_filename( mp->filename() ) << ": Timeline " <<text_buf;
Action::manager().store(info.str());
}
}
}
ImGui::PopID();
}
}
// special case when all media players are (cut to) the same size
else if ( durations.size() > 0) {
// calculate position of icon
double x = static_cast<double>(durations.front()) * width_ratio;
ImGui::SetCursorPos(pos + ImVec2( static_cast<float>(x) - 2.f, framesize.y + _v_space) );
// offer only to adjust size by removing ending gap
if ( mp->timeline()->gapAt( mp->timeline()->end() ) ) {
if ( ImGuiToolkit::ButtonIcon(7, 0, "Remove end gap" )){
if ( mp->timeline()->removeGaptAt(mp->timeline()->end()) ) {
std::ostringstream info;
info << SystemToolkit::base_filename( mp->filename() ) << ": Timeline Remove end gap";
Action::manager().store(info.str());
}
}
}
}
ImGui::PopStyleVar();
ImGui::PopID();
}
// next line position
ImGui::SetCursorPos(image_top + ImVec2(0, 2.0f * _timeline_height + _v_space));
// ImGui::Spacing();
ImGui::SetCursorPos(image_top + ImVec2(0, 2.0f * _timeline_height + 2.f * _v_space));
}
}
@@ -2342,6 +2455,11 @@ void SourceController::RenderSelection(size_t i)
}
///
/// context menu from actions above
///
RenderSelectionContextMenu();
///
/// Play button bar
///
@@ -2391,6 +2509,45 @@ void SourceController::RenderSelection(size_t i)
ImGui::PopStyleColor(4);
}
void SourceController::RenderSelectionContextMenu()
{
if (_selection_mediaplayer == nullptr)
return;
if (_selection_context_menu) {
ImGui::OpenPopup("_speedchange_context_menu");
_selection_context_menu = false;
}
if (ImGui::BeginPopup("_speedchange_context_menu"))
{
std::ostringstream info;
info << SystemToolkit::base_filename( _selection_mediaplayer->filename() );
if ( ImGuiToolkit::MenuItemIcon(14, 16, ICON_FA_CARET_LEFT " Accelerate", false, fabs(_selection_target_faster) > 0 )){
_selection_mediaplayer->setPlaySpeed( _selection_target_faster );
info << ": Speed x" << std::setprecision(3) << _selection_target_faster;
Action::manager().store(info.str());
}
if ( ImGuiToolkit::MenuItemIcon(15, 16, ICON_FA_CARET_RIGHT " Slow down", false, fabs(_selection_target_slower) > 0 )){
_selection_mediaplayer->setPlaySpeed( _selection_target_slower );
info << ": Speed x" << std::setprecision(3) << _selection_target_slower;
Action::manager().store(info.str());
}
if ( _selection_mediaplayer->timeline()->gapAt( _selection_mediaplayer->timeline()->end()) ) {
if ( ImGuiToolkit::MenuItemIcon(7, 0, "Remove end gap" )){
info << ": Remove end gap ";
if ( _selection_mediaplayer->timeline()->removeGaptAt(_selection_mediaplayer->timeline()->end()) )
Action::manager().store(info.str());
}
}
ImGui::EndPopup();
}
}
bool SourceController::SourceButton(Source *s, ImVec2 framesize)
{
bool ret = false;
@@ -2722,13 +2879,22 @@ void SourceController::RenderMediaPlayer(MediaPlayer *mp)
if ( ImGuiToolkit::ButtonIconMultistate(iconsloop, &current_loop) )
mp->setLoop( (MediaPlayer::LoopMode) current_loop );
std::ostringstream oss;
oss << SystemToolkit::base_filename( mp->filename() );
// speed slider (if enough space)
float speed = static_cast<float>(mp->playSpeed());
if ( rendersize.x > _min_width * 1.4f ) {
ImGui::SameLine(0, MAX(_h_space * 2.f, rendersize.x - _min_width * 1.6f) );
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - _buttons_height );
if (ImGui::DragFloat( "##Speed", &speed, 0.01f, -10.f, 10.f, "Speed x %.1f", 2.f))
if (ImGui::DragFloat( "##Speed", &speed, 0.01f, -10.f, 10.f, "Speed " UNICODE_MULTIPLY " %.1f", 2.f))
mp->setPlaySpeed( static_cast<double>(speed) );
if (ImGui::IsItemDeactivatedAfterEdit()){
oss << ": Speed x" << std::setprecision(3) << speed;
Action::manager().store(oss.str());
}
}
///
@@ -2742,15 +2908,18 @@ void SourceController::RenderMediaPlayer(MediaPlayer *mp)
ImGui::OpenPopup( "MenuTimeline" );
if (ImGui::BeginPopup( "MenuTimeline" ))
{
if (ImGui::MenuItem(UNICODE_MULTIPLY " " ICON_FA_CARET_RIGHT " Reset speed" )){
if (ImGuiToolkit::MenuItemIcon(19,15,"Reset speed" )){
speed = 1.f;
mp->setPlaySpeed( static_cast<double>(speed) );
oss << ": Speed x1";
Action::manager().store(oss.str());
}
if (ImGui::MenuItem(ICON_FA_WINDOW_CLOSE " Reset timeline" )){
timeline_zoom = 1.f;
mp->timeline()->clearFading();
mp->timeline()->clearGaps();
Action::manager().store("Timeline Reset");
oss << ": Reset timeline";
Action::manager().store(oss.str());
}
if (ImGui::BeginMenu(ICON_FA_RANDOM " Auto fading"))
{
@@ -2759,15 +2928,19 @@ void SourceController::RenderMediaPlayer(MediaPlayer *mp)
if (ImGui::MenuItem(names[i])) {
mp->timeline()->autoFading( 250 * (int ) pow(2, i) );
mp->timeline()->smoothFading( 2 * (i + 1) );
Action::manager().store("Timeline Auto fading");
oss << ": Timeline Auto fading " << 250 * (int ) pow(2, i);
Action::manager().store(oss.str());
}
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu(ICON_FA_CUT " Auto cut" )){
if (ImGui::MenuItem("Cut faded areas"))
if (mp->timeline()->autoCut())
Action::manager().store("Timeline Auto cut");
if (ImGuiToolkit::MenuItemIcon(14, 12, "Cut faded areas" ))
if (mp->timeline()->autoCut()){
oss << ": Cut faded areas";
Action::manager().store(oss.str());
}
ImGui::EndMenu();
}
@@ -2820,7 +2993,11 @@ void SourceController::RenderMediaPlayer(MediaPlayer *mp)
else if (released)
{
tl->refresh();
Action::manager().store("Timeline change");
if (Settings::application.widget.timeline_editmode)
oss << ": Timeline cut";
else
oss << ": Timeline opacity";
Action::manager().store(oss.str());
}
// custom timeline slider
@@ -2840,10 +3017,27 @@ void SourceController::RenderMediaPlayer(MediaPlayer *mp)
ImGui::SetCursorScreenPos(bottom + ImVec2(1.f, 0.5f * _timeline_height));
if (Settings::application.widget.timeline_editmode) {
// action auto cut
if (ImGuiToolkit::IconButton(9, 3, "Cut at cursor")) {
if (mp->timeline()->cut(mp->position()))
Action::manager().store("Timeline Cut");
// action cut
if (mp->isPlaying()) {
ImGuiToolkit::HelpIcon("Pause video to enable cut options", 9, 3);
}
else if (ImGuiToolkit::IconButton(9, 3, "Cut at cursor")) {
ImGui::OpenPopup("timeline_cut_context_menu");
}
if (ImGui::BeginPopup("timeline_cut_context_menu")){
if (ImGuiToolkit::MenuItemIcon(1,0,"Cut left")){
if (mp->timeline()->cut(mp->position(), true, false)) {
oss << ": Timeline cut";
Action::manager().store(oss.str());
}
}
if (ImGuiToolkit::MenuItemIcon(2,0,"Cut right")){
if (mp->timeline()->cut(mp->position(), false, false)){
oss << ": Timeline cut";
Action::manager().store(oss.str());
}
}
ImGui::EndPopup();
}
}
else {
@@ -2858,7 +3052,8 @@ void SourceController::RenderMediaPlayer(MediaPlayer *mp)
ImGui::PopButtonRepeat();
if (_actionsmooth > 0 && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
Action::manager().store("Timeline Smooth");
oss << ": Timeline opacity smooth";
Action::manager().store(oss.str());
_actionsmooth = 0;
}
}

View File

@@ -120,14 +120,24 @@ class SourceController
InfoVisitor info_;
SourceList selection_;
bool _selection_context_menu;
MediaPlayer *_selection_mediaplayer;
double _selection_target_slower;
double _selection_target_faster;
void RenderSelectionContextMenu();
// re-usable ui parts
void DrawButtonBar(ImVec2 bottom, float width);
const char *SourcePlayIcon(Source *s);
bool SourceButton(Source *s, ImVec2 framesize);
void RenderSelectedSources();
void RenderSelection(size_t i);
void RenderSingleSource(Source *s);
// Render the sources dynamically selected
void RenderSelectedSources();
// Render a stored selection
void RenderSelection(size_t i);
// Render a single source
void RenderSingleSource(Source *s);
// Render a single media player
bool media_playing_mode_;
bool slider_pressed_;
void RenderMediaPlayer(MediaPlayer *mp);

Binary file not shown.