}
if (role == Qt::BackgroundRole) {
if (Column(column) == Column::PLAYING) {
- if (row == currently_playing_index) {
+ auto it = current_progress.find(row);
+ if (it != current_progress.end()) {
+ double play_progress = it->second;
+
// This only really works well for the first column, for whatever odd Qt reason.
QLinearGradient grad(QPointF(0, 0), QPointF(1, 0));
grad.setCoordinateMode(grad.QGradient::ObjectBoundingMode);
switch (Column(column)) {
case Column::PLAYING:
- return (row == currently_playing_index) ? "→" : "";
+ return current_progress.count(row) ? "→" : "";
case Column::IN:
return QString::fromStdString(pts_to_string(clips[row].pts_in));
case Column::OUT:
}
}
+void PlayList::set_progress(const map<size_t, double> &progress)
+{
+ const int column = int(Column::PLAYING);
+ map<size_t, double> old_progress = move(this->current_progress);
+ this->current_progress = progress;
+
+ for (auto it : old_progress) {
+ size_t index = it.first;
+ if (current_progress.count(index) == 0) {
+ emit dataChanged(this->index(index, column), this->index(index, column));
+ }
+ }
+ for (auto it : current_progress) {
+ size_t index = it.first;
+ emit dataChanged(this->index(index, column), this->index(index, column));
+ }
+}
+
namespace {
Clip deserialize_clip(const ClipProto &clip_proto)
#include <QAbstractTableModel>
#include <stdint.h>
+#include <map>
#include <string>
#include <vector>
void set_currently_playing(int index, double progress); // -1 = none.
int get_currently_playing() const { return currently_playing_index; }
+ void set_progress(const std::map<size_t, double> &progress);
+
ClipListProto serialize() const;
void emit_data_changed(size_t row) override;
std::vector<Clip> clips;
int currently_playing_index = -1;
double play_progress = 0.0;
+ std::map<size_t, double> current_progress;
};
#endif // !defined (_CLIP_LIST_H)
});
});
live_player->set_next_clip_callback(bind(&MainWindow::live_player_get_next_clip, this));
- live_player->set_progress_callback([this](double played_this_clip, double total_length) {
- post_to_main_thread([this, played_this_clip, total_length] {
- live_player_clip_progress(played_this_clip, total_length);
+ live_player->set_progress_callback([this](const map<size_t, double> &progress) {
+ post_to_main_thread([this, progress] {
+ live_player_clip_progress(progress);
});
});
set_output_status("paused");
QItemSelectionModel *selected = ui->clip_list->selectionModel();
if (!selected->hasSelection()) {
- preview_player->play_clip(*cliplist_clips->back(), 0);
+ preview_player->play_clip(*cliplist_clips->back(), cliplist_clips->size() - 1, 0);
return;
}
} else {
stream_idx = ui->preview_display->get_stream_idx();
}
- preview_player->play_clip(*cliplist_clips->clip(index.row()), stream_idx);
+ preview_player->play_clip(*cliplist_clips->clip(index.row()), index.row(), stream_idx);
}
void MainWindow::preview_angle_clicked(unsigned stream_idx)
}
const Clip &clip = *playlist_clips->clip(row);
- live_player->play_clip(clip, clip.stream_idx);
+ live_player->play_clip(clip, row, clip.stream_idx);
+ playlist_clips->set_progress({{ row, 0.0f }});
playlist_clips->set_currently_playing(row, 0.0f);
playlist_selection_changed();
}
int row = playlist_clips->get_currently_playing();
if (row == -1 || row == int(playlist_clips->size()) - 1) {
set_output_status("paused");
+ playlist_clips->set_progress({});
playlist_clips->set_currently_playing(-1, 0.0f);
} else {
+ playlist_clips->set_progress({{ row + 1, 0.0f }});
playlist_clips->set_currently_playing(row + 1, 0.0f);
}
}
-Clip MainWindow::live_player_get_next_clip()
+pair<Clip, size_t> MainWindow::live_player_get_next_clip()
{
// playlist_clips can only be accessed on the main thread.
// Hopefully, we won't have to wait too long for this to come back.
- promise<Clip> clip_promise;
- future<Clip> clip = clip_promise.get_future();
+ promise<pair<Clip, size_t>> clip_promise;
+ future<pair<Clip, size_t>> clip = clip_promise.get_future();
post_to_main_thread([this, &clip_promise] {
int row = playlist_clips->get_currently_playing();
if (row != -1 && row < int(playlist_clips->size()) - 1) {
- clip_promise.set_value(*playlist_clips->clip(row + 1));
+ clip_promise.set_value(make_pair(*playlist_clips->clip(row + 1), row + 1));
} else {
- clip_promise.set_value(Clip());
+ clip_promise.set_value(make_pair(Clip(), 0));
}
});
return clip.get();
return buf;
}
-void MainWindow::live_player_clip_progress(double played_this_clip, double total_length)
+void MainWindow::live_player_clip_progress(const map<size_t, double> &progress)
{
- playlist_clips->set_currently_playing(playlist_clips->get_currently_playing(), played_this_clip / total_length);
-
- double remaining = total_length - played_this_clip;
- for (int row = playlist_clips->get_currently_playing() + 1; row < int(playlist_clips->size()); ++row) {
+ playlist_clips->set_progress(progress);
+
+ // Look at the last clip and then start counting from there.
+ assert(!progress.empty());
+ auto last_it = progress.end();
+ --last_it;
+ double remaining = 0.0;
+ double last_fade_time_seconds = 0.0;
+ for (size_t row = last_it->first; row < playlist_clips->size(); ++row) {
const Clip clip = *playlist_clips->clip(row);
- remaining += double(clip.pts_out - clip.pts_in) / TIMEBASE / 0.5; // FIXME: stop hardcoding speed.
+ double clip_length = double(clip.pts_out - clip.pts_in) / TIMEBASE / 0.5; // FIXME: stop hardcoding speed.
+ if (row == last_it->first) {
+ // A clip we're playing: Subtract the part we've already played.
+ remaining = clip_length * (1.0 - last_it->second);
+ } else {
+ // A clip we haven't played yet: Subtract the part that's overlapping
+ // with a previous clip (due to fade).
+ remaining += max(clip_length - last_fade_time_seconds, 0.0);
+ }
+ last_fade_time_seconds = min(clip_length, clip.fade_time_seconds);
}
set_output_status(format_duration(remaining) + " left");
}
Clip fake_clip;
fake_clip.pts_in = pts;
fake_clip.pts_out = pts + 1;
- preview_player->play_clip(fake_clip, stream_idx);
+ preview_player->play_clip(fake_clip, 0, stream_idx);
}
void MainWindow::playlist_selection_changed()
void preview_angle_clicked(unsigned stream_idx);
void play_clicked();
void live_player_clip_done();
- Clip live_player_get_next_clip();
- void live_player_clip_progress(double played_this_clip, double total_length);
+ std::pair<Clip, size_t> live_player_get_next_clip();
+ void live_player_clip_progress(const std::map<size_t, double> &progress);
void set_output_status(const std::string &status);
void playlist_duplicate();
void playlist_remove();
constexpr double output_framerate = 60000.0 / 1001.0; // FIXME: make configurable
int64_t pts = 0;
Clip next_clip;
+ size_t next_clip_idx = size_t(-1);
bool got_next_clip = false;
double next_clip_fade_time = -1.0;
}
Clip clip;
+ size_t clip_idx;
unsigned stream_idx;
{
lock_guard<mutex> lock(mu);
clip = current_clip;
+ clip_idx = current_clip_idx;
stream_idx = current_stream_idx;
}
steady_clock::time_point origin = steady_clock::now(); // TODO: Add a 100 ms buffer for ramp-up?
double time_left_this_clip = double(clip.pts_out - in_pts) / TIMEBASE / speed;
if (!got_next_clip && next_clip_callback != nullptr && time_left_this_clip <= clip.fade_time_seconds) {
// Find the next clip so that we can begin a fade.
- next_clip = next_clip_callback();
+ tie(next_clip, next_clip_idx) = next_clip_callback();
if (next_clip.pts_in != -1) {
got_next_clip = true;
}
}
+ // pts not affected by the swapping below.
+ int64_t in_pts_for_progress = in_pts, in_pts_secondary_for_progress = -1;
+
int primary_stream_idx = stream_idx;
int secondary_stream_idx = -1;
int64_t secondary_pts = -1;
if (got_next_clip && time_left_this_clip <= next_clip_fade_time) {
secondary_stream_idx = next_clip.stream_idx;
in_pts_secondary = lrint(next_clip.pts_in + (next_clip_fade_time - time_left_this_clip) * TIMEBASE * speed);
+ in_pts_secondary_for_progress = in_pts_secondary;
fade_alpha = 1.0f - time_left_this_clip / next_clip_fade_time;
// If more than half-way through the fade, interpolate the next clip
if (progress_callback != nullptr) {
// NOTE: None of this will take into account any snapping done below.
- double played_this_clip = double(in_pts - clip.pts_in) / TIMEBASE / speed;
+ double played_this_clip = double(in_pts_for_progress - clip.pts_in) / TIMEBASE / speed;
double total_length = double(clip.pts_out - clip.pts_in) / TIMEBASE / speed;
- progress_callback(played_this_clip, total_length);
+ map<size_t, double> progress{{ clip_idx, played_this_clip / total_length }};
+
+ if (got_next_clip && time_left_this_clip <= next_clip_fade_time) {
+ double played_next_clip = double(in_pts_secondary_for_progress - next_clip.pts_in) / TIMEBASE / speed;
+ double total_next_length = double(next_clip.pts_out - next_clip.pts_in) / TIMEBASE / speed;
+ progress[next_clip_idx] = played_next_clip / total_next_length;
+ }
+ progress_callback(progress);
}
int64_t in_pts_lower, in_pts_upper;
// Last-ditch effort to get the next clip (if e.g. the fade time was zero seconds).
if (!got_next_clip && next_clip_callback != nullptr) {
- next_clip = next_clip_callback();
+ tie(next_clip, next_clip_idx) = next_clip_callback();
if (next_clip.pts_in != -1) {
got_next_clip = true;
in_pts_start_next_clip = next_clip.pts_in;
// Switch to next clip if we got it.
if (got_next_clip) {
clip = next_clip;
+ clip_idx = next_clip_idx;
stream_idx = next_clip.stream_idx; // Override is used for previews only, and next_clip is used for live ony.
if (done_callback != nullptr) {
done_callback();
thread(&Player::thread_func, this, also_output_to_stream).detach();
}
-void Player::play_clip(const Clip &clip, unsigned stream_idx)
+void Player::play_clip(const Clip &clip, size_t clip_idx, unsigned stream_idx)
{
{
lock_guard<mutex> lock(mu);
current_clip = clip;
current_stream_idx = stream_idx;
+ current_clip_idx = clip_idx;
}
{
public:
Player(JPEGFrameView *destination, bool also_output_to_stream);
- void play_clip(const Clip &clip, unsigned stream_idx);
+ void play_clip(const Clip &clip, size_t clip_idx, unsigned stream_idx);
void override_angle(unsigned stream_idx); // For the current clip only.
// Not thread-safe to set concurrently with playing.
// Not thread-safe to set concurrently with playing.
// Will be called back from the player thread.
- using next_clip_callback_func = std::function<Clip()>;
+ // The second parameter is the clip's position in the play list.
+ using next_clip_callback_func = std::function<std::pair<Clip, size_t>()>;
void set_next_clip_callback(next_clip_callback_func cb) { next_clip_callback = cb; }
// Not thread-safe to set concurrently with playing.
// Will be called back from the player thread.
- using progress_callback_func = std::function<void(double played_this_clip, double total_length)>;
+ using progress_callback_func = std::function<void(const std::map<size_t, double> &progress)>;
void set_progress_callback(progress_callback_func cb) { progress_callback = cb; }
// QueueInterface.
std::mutex mu;
Clip current_clip; // Under mu. Can have pts_in = -1 for no clip.
+ size_t current_clip_idx; // Under mu.
unsigned current_stream_idx; // Under mu.
std::mutex queue_state_mu;