From d32c4c75ef1cf4d69474a0a0e8f8127e77910099 Mon Sep 17 00:00:00 2001 From: wm4 Date: Fri, 13 Nov 2015 22:45:40 +0100 Subject: 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. --- player/video.c | 186 +++++++++++++++++++++++++-------------------------------- 1 file changed, 83 insertions(+), 103 deletions(-) (limited to 'player/video.c') diff --git a/player/video.c b/player/video.c index 430fed530e..398d74a643 100644 --- a/player/video.c +++ b/player/video.c @@ -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; - } - - 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; + 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; } - - 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) - 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; + 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; - } - 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); -- cgit v1.2.3