player: refactor display-sync frame duration calculations

Get rid of get_past_frame_durations(), which was a bit too messy. Add
a past_frames array, which contains the same information in a more
reasonable way. This also means that we can get the exact current and
past frame durations without going through awful stuff. (The main
problem is that vo_pts_history contains future frames as well, which is
needed for frame backstepping etc., but gets in the way here.)

Also disable the automatic disabling of display-sync if the frame
duration changes, and extend the frame durations allowed for display
sync. To allow arbitrarily high durations, vo.c needs to be changed
to pause and potentially redraw OSD while showing a single frame, so
they're still limited.

In an attempt to deal with VFR, calculate the overall speed using the
average FPS. The frame scheduling itself does not use the average FPS,
but the duration of the current frame. This does not work too well,
but provides a good base for further improvements.

Where this commit actually helps a lot is dealing with rounded
timestamps, e.g. if the container framerate is wrong or unknown, or
if the muxer wrote incorrectly rounded timestamps. While the rounding
errors apparently can't be get rid of completely in the general case,
this is still much better than e.g. disabling display-sync completely
just because some frame durations go out of bounds.
This commit is contained in:
wm4 2015-11-13 22:45:40 +01:00
parent 624c9e46ce
commit d32c4c75ef
5 changed files with 98 additions and 134 deletions

View File

@ -2687,10 +2687,10 @@ static int mp_property_vf_fps(void *ctx, struct m_property *prop,
MPContext *mpctx = ctx;
if (!mpctx->d_video)
return M_PROPERTY_UNAVAILABLE;
double res = stabilize_frame_duration(mpctx, false);
if (res <= 0)
double avg = calc_average_frame_duration(mpctx);
if (avg <= 0)
return M_PROPERTY_UNAVAILABLE;
return m_property_double_ro(action, arg, 1 / res);
return m_property_double_ro(action, arg, 1.0 / avg);
}
/// Video aspect (RO)

View File

@ -76,9 +76,6 @@ enum seek_precision {
MPSEEK_VERY_EXACT,
};
// Comes from the assumption that some formats round timestamps to ms.
#define FRAME_DURATION_TOLERANCE 0.0011
enum video_sync {
VS_DEFAULT = 0,
VS_DISP_RESAMPLE,
@ -97,6 +94,13 @@ enum video_sync {
(x) == VS_DISP_VDROP || \
(x) == VS_DISP_NONE)
// Information about past video frames that have been sent to the VO.
struct frame_info {
double pts;
double duration; // PTS difference to next frame
double approx_duration; // possibly fixed/smoothed out duration
};
struct track {
enum stream_type type;
@ -264,7 +268,6 @@ typedef struct MPContext {
double audio_speed, video_speed;
bool display_sync_active;
bool broken_fps_header;
double display_sync_frameduration;
int display_sync_drift_dir;
// Timing error (in seconds) due to rounding on vsync boundaries
double display_sync_error;
@ -321,6 +324,10 @@ typedef struct MPContext {
uint64_t vo_pts_history_seek_ts;
uint64_t backstep_start_seek_ts;
bool backstep_active;
// Past timestamps etc. (stupidly duplicated with vo_pts_history).
// The newest frame is at index 0.
struct frame_info *past_frames;
int num_past_frames;
double next_heartbeat;
double last_idle_tick;
@ -498,7 +505,6 @@ void mp_idle(struct MPContext *mpctx);
void idle_loop(struct MPContext *mpctx);
int handle_force_window(struct MPContext *mpctx, bool force);
void add_frame_pts(struct MPContext *mpctx, double pts);
int get_past_frame_durations(struct MPContext *mpctx, double *fd, int num);
void seek_to_last_frame(struct MPContext *mpctx);
// scripting.c
@ -528,6 +534,6 @@ void write_video(struct MPContext *mpctx, double endpts);
void mp_force_video_refresh(struct MPContext *mpctx);
void uninit_video_out(struct MPContext *mpctx);
void uninit_video_chain(struct MPContext *mpctx);
double stabilize_frame_duration(struct MPContext *mpctx, bool require_exact);
double calc_average_frame_duration(struct MPContext *mpctx);
#endif /* MPLAYER_MP_CORE_H */

View File

@ -1034,7 +1034,6 @@ static void play_current_file(struct MPContext *mpctx)
mpctx->max_frames = -1;
mpctx->video_speed = mpctx->audio_speed = opts->playback_speed;
mpctx->speed_factor_a = mpctx->speed_factor_v = 1.0;
mpctx->display_sync_frameduration = 0.0;
mpctx->display_sync_error = 0.0;
mpctx->broken_fps_header = false;
mpctx->display_sync_active = false;

View File

@ -693,27 +693,6 @@ void add_frame_pts(struct MPContext *mpctx, double pts)
mpctx->vo_pts_history_pts[0] = pts;
}
// Return the last (at most num) frame duration in fd[]. Return the number of
// entries written to fd[] (range [0, num]). fd[0] is the most recent frame.
int get_past_frame_durations(struct MPContext *mpctx, double *fd, int num)
{
double next_pts = mpctx->vo_pts_history_pts[0];
if (mpctx->vo_pts_history_seek[0] != mpctx->vo_pts_history_seek_ts ||
next_pts == MP_NOPTS_VALUE)
return 0;
int num_ret = 0;
for (int n = 1; n < MAX_NUM_VO_PTS && num_ret < num; n++) {
double frame_pts = mpctx->vo_pts_history_pts[n];
// Discontinuity -> refuse to return a value.
if (mpctx->vo_pts_history_seek[n] != mpctx->vo_pts_history_seek_ts ||
next_pts <= frame_pts || frame_pts == MP_NOPTS_VALUE)
break;
fd[num_ret++] = next_pts - frame_pts;
next_pts = frame_pts;
}
return num_ret;
}
static double find_previous_pts(struct MPContext *mpctx, double pts)
{
for (int n = 0; n < MAX_NUM_VO_PTS - 1; n++) {

View File

@ -206,6 +206,7 @@ void reset_video_state(struct MPContext *mpctx)
mpctx->time_frame = 0;
mpctx->video_pts = MP_NOPTS_VALUE;
mpctx->video_next_pts = MP_NOPTS_VALUE;
mpctx->num_past_frames = 0;
mpctx->total_avsync_change = 0;
mpctx->last_av_difference = 0;
mpctx->display_sync_disable_counter = 0;
@ -799,76 +800,18 @@ static void init_vo(struct MPContext *mpctx)
mp_notify(mpctx, MPV_EVENT_VIDEO_RECONFIG, NULL);
}
// Attempt to stabilize frame duration from jittery timestamps. This is mostly
// needed with semi-broken file formats which round timestamps to ms, or files
// created from them.
// We do this to make a stable decision how much to change video playback speed.
// Otherwise calc_best_speed() could make a different decision every frame, and
// also audio speed would have to be readjusted all the time.
// Return -1 if the frame duration seems to be unstable.
// If require_exact is false, just return the average frame duration on failure.
double stabilize_frame_duration(struct MPContext *mpctx, bool require_exact)
double calc_average_frame_duration(struct MPContext *mpctx)
{
if (require_exact && mpctx->broken_fps_header)
return -1;
// Note: the past frame durations are raw and unadjusted.
double fd[10];
int num = get_past_frame_durations(mpctx, fd, MP_ARRAY_SIZE(fd));
if (num < MP_ARRAY_SIZE(fd))
return -1;
bool ok = true;
double min = fd[0];
double max = fd[0];
double total_duration = 0;
for (int n = 0; n < num; n++) {
double cur = fd[n];
if (fabs(cur - fd[num - 1]) > FRAME_DURATION_TOLERANCE)
ok = false;
min = MPMIN(min, cur);
max = MPMAX(max, cur);
total_duration += cur;
double total = 0;
int num = 0;
for (int n = 0; n < mpctx->num_past_frames; n++) {
double dur = mpctx->past_frames[0].approx_duration;
if (dur <= 0)
continue;
total += dur;
num += 1;
}
if (max - min > FRAME_DURATION_TOLERANCE || !ok)
goto fail;
// It's not really possible to compute the actual, correct FPS, unless we
// e.g. consider a list of potentially correct values, detect cycles, or
// use similar guessing methods.
// Naively using the average between min and max should give a stable, but
// still relatively close value.
double modified_duration = (min + max) / 2;
// Except for the demuxer reported FPS, which might be the correct one.
// VFR files could contain segments that don't match.
if (mpctx->d_video->fps > 0) {
double demux_duration = 1.0 / mpctx->d_video->fps;
if (fabs(modified_duration - demux_duration) <= FRAME_DURATION_TOLERANCE)
modified_duration = demux_duration;
}
// Verify the estimated stabilized frame duration with the actual time
// passed in these frames. If it's wrong (wrong FPS in the header), then
// this will deviate a bit.
if (fabs(total_duration - modified_duration * num) > FRAME_DURATION_TOLERANCE)
{
if (require_exact && !mpctx->broken_fps_header) {
// The error message is slightly misleading: a framerate header
// field is not really needed, as long as the file has an exact
// timebase.
MP_WARN(mpctx, "File has broken or missing framerate header\n"
"field, or is VFR with broken timestamps.\n");
mpctx->broken_fps_header = true;
}
goto fail;
}
return modified_duration;
fail:
return require_exact ? -1 : total_duration / num;
return num > 0 ? total / num : 0;
}
static bool using_spdif_passthrough(struct MPContext *mpctx)
@ -969,24 +912,17 @@ static void handle_display_sync_frame(struct MPContext *mpctx,
if (vsync <= 0)
goto done;
double adjusted_duration = stabilize_frame_duration(mpctx, true);
if (adjusted_duration >= 0)
adjusted_duration /= opts->playback_speed;
if (adjusted_duration <= 0.002 || adjusted_duration > 0.05)
double adjusted_duration = mpctx->past_frames[0].approx_duration;
double avg_duration = calc_average_frame_duration(mpctx);
adjusted_duration /= opts->playback_speed;
avg_duration /= opts->playback_speed;
if (adjusted_duration <= 0.001 || adjusted_duration > 0.5)
goto done;
double prev_duration = mpctx->display_sync_frameduration;
mpctx->display_sync_frameduration = adjusted_duration;
if (adjusted_duration != prev_duration) {
mpctx->display_sync_disable_counter = 50;
goto done;
}
mpctx->speed_factor_v = calc_best_speed(mpctx, vsync, adjusted_duration);
if (mpctx->speed_factor_v <= 0) {
mpctx->speed_factor_v = calc_best_speed(mpctx, vsync, avg_duration);
// If it doesn't work, play at normal speed.
if (mpctx->speed_factor_v <= 0)
mpctx->speed_factor_v = 1.0;
goto done;
}
double av_diff = mpctx->last_av_difference;
if (fabs(av_diff) > 0.5)
@ -1085,26 +1021,58 @@ static void schedule_frame(struct MPContext *mpctx, struct vo_frame *frame)
}
}
// Return the next frame duration as stored in the file.
// frame=0 means the current frame, 1 the frame after that etc.
// Can return -1, though usually will return a fallback if frame unavailable.
static double get_frame_duration(struct MPContext *mpctx, int frame)
// Determine the mpctx->past_frames[0] frame duration.
static void calculate_frame_duration(struct MPContext *mpctx)
{
struct MPOpts *opts = mpctx->opts;
struct vo *vo = mpctx->video_out;
assert(mpctx->num_past_frames >= 1 && mpctx->num_next_frames >= 1);
double diff = -1;
if (frame + 2 <= mpctx->num_next_frames) {
double vpts0 = mpctx->next_frames[frame]->pts;
double vpts1 = mpctx->next_frames[frame + 1]->pts;
if (vpts0 != MP_NOPTS_VALUE && vpts1 != MP_NOPTS_VALUE)
diff = vpts1 - vpts0;
double demux_duration =
mpctx->d_video->fps > 0 ? 1.0 / mpctx->d_video->fps : -1;
double duration = -1;
if (mpctx->num_next_frames >= 2) {
double pts0 = mpctx->next_frames[0]->pts;
double pts1 = mpctx->next_frames[1]->pts;
if (pts0 != MP_NOPTS_VALUE && pts1 != MP_NOPTS_VALUE && pts1 >= pts0)
duration = pts1 - pts0;
} else {
// E.g. last frame on EOF.
duration = demux_duration;
}
if (diff < 0 && mpctx->d_video->fps > 0)
diff = 1.0 / mpctx->d_video->fps; // fallback to demuxer-reported fps
if (opts->untimed || vo->driver->untimed)
diff = -1; // disable frame dropping and aspects of frame timing
return diff;
// The following code tries to compensate for rounded Matroska timestamps
// by "unrounding" frame durations, or if not possible, approximating them.
// These formats usually round on 1ms. (Some muxers do this incorrectly,
// and might be off by 2ms or more, and compensate for it later by an
// equal rounding error into the opposite direction. Don't try to deal
// with them; too much potential damage to timing.)
double tolerance = 0.0011;
double total = 0;
int num_dur = 0;
for (int n = 1; n < mpctx->num_past_frames; n++) {
// Eliminate likely outliers using a really dumb heuristic.
double dur = mpctx->past_frames[n].duration;
if (dur <= 0 || fabs(dur - duration) >= tolerance)
break;
total += dur;
num_dur += 1;
}
// Try if the demuxer frame rate fits - if so, just take it.
double approx_duration = duration;
if (demux_duration > 0) {
// Note that even if each timestamp is within rounding tolerance, it
// could literally not add up (e.g. if demuxer FPS is rounded itself).
if (fabs(duration - demux_duration) < tolerance &&
fabs(total - demux_duration * num_dur) < tolerance)
{
approx_duration = demux_duration;
}
}
mpctx->past_frames[0].duration = duration;
mpctx->past_frames[0].approx_duration = approx_duration;
}
void write_video(struct MPContext *mpctx, double endpts)
@ -1190,6 +1158,16 @@ void write_video(struct MPContext *mpctx, double endpts)
}
assert(mpctx->num_next_frames >= 1);
if (mpctx->num_past_frames >= MAX_NUM_VO_PTS)
mpctx->num_past_frames--;
MP_TARRAY_INSERT_AT(mpctx, mpctx->past_frames, mpctx->num_past_frames, 0,
(struct frame_info){0});
struct frame_info *frame_info = &mpctx->past_frames[0];
frame_info->pts = mpctx->next_frames[0]->pts;
calculate_frame_duration(mpctx);
struct vo_frame dummy = {
.pts = pts,
.duration = -1,
@ -1201,7 +1179,9 @@ void write_video(struct MPContext *mpctx, double endpts)
dummy.frames[n] = mpctx->next_frames[n];
struct vo_frame *frame = vo_frame_ref(&dummy);
double diff = get_frame_duration(mpctx, 0);
double diff = frame_info->approx_duration;
if (opts->untimed || vo->driver->untimed)
diff = -1; // disable frame dropping and aspects of frame timing
if (diff >= 0) {
// expected A/V sync correction is ignored
diff /= mpctx->video_speed;
@ -1214,6 +1194,8 @@ void write_video(struct MPContext *mpctx, double endpts)
mpctx->last_vo_pts = mpctx->video_pts;
mpctx->playback_pts = mpctx->video_pts;
shift_frames(mpctx);
schedule_frame(mpctx, frame);
mpctx->osd_force_update = true;
@ -1222,8 +1204,6 @@ void write_video(struct MPContext *mpctx, double endpts)
vo_queue_frame(vo, frame);
shift_frames(mpctx);
// The frames were shifted down; "initialize" the new first entry.
if (mpctx->num_next_frames >= 1)
handle_new_frame(mpctx);