+double calc_progress(const Clip &clip, int64_t pts)
+{
+ return double(pts - clip.pts_in) / (clip.pts_out - clip.pts_in);
+}
+
+void do_splice(const vector<ClipWithID> &new_list, size_t playing_index1, ssize_t playing_index2, vector<ClipWithID> *old_list)
+{
+ assert(playing_index2 == -1 || size_t(playing_index2) == playing_index1 + 1);
+
+ // First see if we can do the simple thing; find an element in the new
+ // list that we are already playing, which will serve as our splice point.
+ int splice_start_new_list = -1;
+ for (size_t clip_idx = 0; clip_idx < new_list.size(); ++clip_idx) {
+ if (new_list[clip_idx].id == (*old_list)[playing_index1].id) {
+ splice_start_new_list = clip_idx + 1;
+ } else if (playing_index2 != -1 && new_list[clip_idx].id == (*old_list)[playing_index2].id) {
+ splice_start_new_list = clip_idx + 1;
+ }
+ }
+ if (splice_start_new_list == -1) {
+ // OK, so the playing items are no longer in the new list. Most likely,
+ // that means we deleted some range that included them. But the ones
+ // before should stay put -- and we don't want to play them. So find
+ // the ones that we've already played, and ignore them. Hopefully,
+ // they're contiguous; the last one that's not seen will be our cut point.
+ //
+ // Keeping track of the playlist range explicitly in the UI would remove
+ // the need for these heuristics, but it would probably also mean we'd
+ // have to lock the playing clip, which sounds annoying.
+ unordered_map<uint64_t, size_t> played_ids;
+ for (size_t clip_idx = 0; clip_idx < playing_index1; ++old_list) {
+ played_ids.emplace((*old_list)[clip_idx].id, clip_idx);
+ }
+ for (size_t clip_idx = 0; clip_idx < new_list.size(); ++clip_idx) {
+ if (played_ids.count(new_list[clip_idx].id)) {
+ splice_start_new_list = clip_idx + 1;
+ }
+ }
+
+ if (splice_start_new_list == -1) {
+ // OK, we didn't find any matches; the lists are totally distinct.
+ // So probably the entire thing was deleted; leave it alone.
+ return;
+ }
+ }
+
+ size_t splice_start_old_list = ((playing_index2 == -1) ? playing_index1 : playing_index2) + 1;
+ old_list->erase(old_list->begin() + splice_start_old_list, old_list->end());
+ old_list->insert(old_list->end(), new_list.begin() + splice_start_new_list, new_list.end());
+}
+
+// Keeps track of the various timelines (wall clock time, output pts,
+// position in the clip we are playing). Generally we keep an origin
+// and assume we increase linearly from there; the intention is to
+// avoid getting compounded accuracy errors, although with double,
+// that is perhaps overkill. (Whenever we break the linear assumption,
+// we need to reset said origin.)
+class TimelineTracker
+{
+public:
+ struct Instant {
+ steady_clock::time_point wallclock_time;
+ int64_t in_pts;
+ int64_t out_pts;
+ int64_t frameno;
+ };
+
+ TimelineTracker(double master_speed, int64_t out_pts_origin)
+ : master_speed(master_speed), last_out_pts(out_pts_origin) {
+ origin.out_pts = out_pts_origin;
+ }
+
+ void new_clip(steady_clock::time_point wallclock_origin, const Clip *clip, int64_t start_pts_offset)
+ {
+ this->clip = clip;
+ origin.wallclock_time = wallclock_origin;
+ origin.in_pts = clip->pts_in + start_pts_offset;
+ origin.out_pts = last_out_pts;
+ origin.frameno = 0;
+ }
+
+ // Returns the current time for said frame.
+ Instant advance_to_frame(int64_t frameno);
+
+ int64_t get_in_pts_origin() const { return origin.in_pts; }
+ bool playing_at_normal_speed() const {
+ const double effective_speed = clip->speed * master_speed;
+ return effective_speed >= 0.999 && effective_speed <= 1.001;
+ }
+
+ void snap_by(int64_t offset) {
+ origin.in_pts += offset;
+ }
+
+ void change_master_speed(double new_master_speed, Instant now);
+
+private:
+ double master_speed;
+ const Clip *clip = nullptr;
+ Instant origin;
+ int64_t last_out_pts;
+};
+
+TimelineTracker::Instant TimelineTracker::advance_to_frame(int64_t frameno)
+{
+ Instant ret;
+ double in_pts_double = origin.in_pts + TIMEBASE * clip->speed * (frameno - origin.frameno) * master_speed / global_flags.output_framerate;
+ ret.in_pts = lrint(in_pts_double);
+ double out_pts_double = origin.out_pts + TIMEBASE * (frameno - origin.frameno) / global_flags.output_framerate;
+ ret.out_pts = lrint(out_pts_double);
+ ret.wallclock_time = origin.wallclock_time + microseconds(lrint((out_pts_double - origin.out_pts) * 1e6 / TIMEBASE));
+ ret.frameno = frameno;
+
+ last_out_pts = ret.out_pts;
+
+ return ret;
+}
+
+void TimelineTracker::change_master_speed(double new_master_speed, Instant now)
+{
+ master_speed = new_master_speed;
+
+ // Reset the origins, since the calculations depend on linear interpolation
+ // based on the master speed.
+ origin = now;
+}
+
+} // namespace
+
+void Player::play_playlist_once()
+{
+ vector<ClipWithID> clip_list;
+ bool clip_ready;
+ steady_clock::time_point before_sleep = steady_clock::now();
+ string pause_status;
+
+ // Wait until we're supposed to play something.
+ {
+ unique_lock<mutex> lock(queue_state_mu);
+ playing = false;
+ clip_ready = new_clip_changed.wait_for(lock, milliseconds(100), [this] {
+ return should_quit || new_clip_ready;
+ });
+ if (should_quit) {
+ return;
+ }
+ if (clip_ready) {