Skip to content

Commit

Permalink
Improve A/V sync when playing close to end of in-progress recordings.
Browse files Browse the repository at this point in the history
MythTV has long had issues quickly achieving stable, A/V sync when
playing close to the end of an in-progress recording.  This is most
easily seen when entering live TV or changing channels.  The previous
round of changes in this area were an improvement but still leaves
cases where there are small stutters every few seconds that never end.
This commit attempts to fix that.

The main, previous change in this area pauses the audio when video
buffering occurs and near the end.  The audio is unpaused by the
normal, A/V, sync code when it thinks the sync is close enough.  This
commit tweaks that behavior to keep the audio paused until the video
has fully caught up.  I've been using this patch for several months
with no ill effects so I think it's safe to commit to master.

Note that the patch looks bigger than it really is due to the
re-ordering of an if statement in MythPlayer::PrebufferEnoughFrames().
That change makes an early return cleaner and simplifies the logic
flow.

Also note that Music Choice channels still present challenging
problems for MythTV.  mythplayer is not really designed for content
with very, low, video, frame rates.  The MusicChoiceEnabled setting
still needs to be set to reliably play NC channels.  With this change,
however, I think it should be easier to automatically detect MC
channels without the need of a special setting.  That hasn't been done
yet, though.
  • Loading branch information
gigem committed Aug 5, 2022
1 parent 26e174d commit 10c2a67
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 132 deletions.
183 changes: 86 additions & 97 deletions mythtv/libs/libmythtv/mythplayer.cpp
Expand Up @@ -76,6 +76,9 @@ const double MythPlayer::kInaccuracyEditor = 0.5;
// keyframe that is closest to the target.
const double MythPlayer::kInaccuracyFull = -1.0;

// How close we can seek to the end of a recording.
const double MythPlayer::kSeekToEndOffset = 1.0;

MythPlayer::MythPlayer(PlayerContext* Context, PlayerFlags Flags)
: m_playerCtx(Context),
m_playerFlags(Flags),
Expand Down Expand Up @@ -726,115 +729,104 @@ bool MythPlayer::PrebufferEnoughFrames(int min_buffers)
if (!m_videoOutput)
return false;

if (!min_buffers
&& (FlagIsSet(kMusicChoice)
|| abs(m_ffrewSkip) > 1
|| GetEof() != kEofStateNone))
min_buffers = 1;

auto wait = false;
if (min_buffers)
wait = m_videoOutput->ValidVideoFrames() < min_buffers;
else if (GetEof() != kEofStateNone)
wait = false;
else if (abs(m_ffrewSkip) > 1)
wait = !m_videoOutput->ValidVideoFrames();
else
wait = !m_videoOutput->EnoughDecodedFrames();

if (wait)
if (!wait)
{
SetBuffering(true);
if (!m_avSync.GetAVSyncAudioPause() && m_audio.IsPaused())
m_audio.Pause(false);
SetBuffering(false);
return m_videoOutput->ValidVideoFrames();
}

// This piece of code is to address the problem, when starting
// Live TV, of jerking and stuttering. Without this code
// that could go on forever, but is cured by a pause and play.
// This code inserts a brief pause and play when the potential
// for the jerking is detected.
SetBuffering(true);

if ((m_liveTV || IsWatchingInprogress())
&& !FlagIsSet(kMusicChoice)
&& m_ffrewSkip == 1)
// This piece of code is to address the problem, when starting
// Live TV, of jerking and stuttering. Without this code
// that could go on forever, but is cured by a pause and play.
// This code inserts a brief pause and play when the potential
// for the jerking is detected.
if ((m_liveTV || IsWatchingInprogress())
&& !FlagIsSet(kMusicChoice)
&& m_ffrewSkip == 1
&& m_avSync.GetAVSyncAudioPause() != kAVSyncAudioPausedLiveTV)
{
auto behind = (GetCurrentFrameCount() - m_framesPlayed) /
m_videoFrameRate;
if (behind < 3.0)
{
uint64_t frameCount = GetCurrentFrameCount();
uint64_t framesLeft = frameCount - m_framesPlayed;
auto margin = static_cast<uint64_t>(m_videoFrameRate * 3.0);
if (framesLeft < margin)
{
if (m_avSync.ResetAVSyncForLiveTV(&m_audio))
{
LOG(VB_PLAYBACK, LOG_NOTICE, LOC + "Pause to allow live tv catch up");
LOG(VB_PLAYBACK, LOG_NOTICE, LOC + QString("Played: %1 Avail: %2 Buffered: %3 Margin: %4")
.arg(m_framesPlayed).arg(frameCount)
.arg(m_videoOutput->ValidVideoFrames()).arg(margin));
}
}
LOG(VB_PLAYBACK, LOG_NOTICE, LOC +
"Pause to allow live tv catch up");
m_avSync.ResetAVSyncForLiveTV(&m_audio);
}
}

std::this_thread::sleep_for(m_frameInterval / 8);
auto waited_for = std::chrono::milliseconds(m_bufferingStart.msecsTo(QTime::currentTime()));
auto last_msg = std::chrono::milliseconds(m_bufferingLastMsg.msecsTo(QTime::currentTime()));
if (last_msg > 100ms && !FlagIsSet(kMusicChoice))
{
if (++m_bufferingCounter == 10)
LOG(VB_GENERAL, LOG_NOTICE, LOC +
"To see more buffering messages use -v playback");
if (m_bufferingCounter >= 10)
{
LOG(VB_PLAYBACK, LOG_NOTICE, LOC +
QString("Waited %1ms for video buffers %2")
.arg(waited_for.count()).arg(m_videoOutput->GetFrameStatus()));
}
else
{
LOG(VB_GENERAL, LOG_NOTICE, LOC +
QString("Waited %1ms for video buffers %2")
.arg(waited_for.count()).arg(m_videoOutput->GetFrameStatus()));
}
m_bufferingLastMsg = QTime::currentTime();
if (m_audio.IsBufferAlmostFull() && m_framesPlayed < 5
&& gCoreContext->GetBoolSetting("MusicChoiceEnabled", false))
{
m_playerFlags = static_cast<PlayerFlags>(m_playerFlags | kMusicChoice);
LOG(VB_GENERAL, LOG_NOTICE, LOC + "Music Choice program detected - disabling AV Sync.");
m_avSync.SetAVSyncMusicChoice(&m_audio);
}
if (waited_for > 7s && m_audio.IsBufferAlmostFull()
&& !FlagIsSet(kMusicChoice))
{
// We are likely to enter this condition
// if the audio buffer was too full during GetFrame in AVFD
LOG(VB_GENERAL, LOG_NOTICE, LOC + "Resetting audio buffer");
m_audio.Reset();
}
// Finish audio pause for sync after 1 second
// in case of infrequent video frames (e.g. music choice)
if (m_avSync.GetAVSyncAudioPause() && waited_for > 1s)
m_avSync.SetAVSyncMusicChoice(&m_audio);
}
std::chrono::milliseconds msecs { 500ms };
if (preBufferDebug)
msecs = 30min;
if ((waited_for > msecs) && !m_videoOutput->EnoughFreeFrames())
{
std::this_thread::sleep_for(m_frameInterval / 8);
auto waited_for = std::chrono::milliseconds(m_bufferingStart.msecsTo(QTime::currentTime()));
auto last_msg = std::chrono::milliseconds(m_bufferingLastMsg.msecsTo(QTime::currentTime()));
if (last_msg > 100ms && !FlagIsSet(kMusicChoice))
{
if (++m_bufferingCounter == 10)
LOG(VB_GENERAL, LOG_NOTICE, LOC +
"Timed out waiting for frames, and"
"\n\t\t\tthere are not enough free frames. "
"Discarding buffered frames.");
// This call will result in some ugly frames, but allows us
// to recover from serious problems if frames get leaked.
DiscardVideoFrames(true, true);
"To see more buffering messages use -v playback");
LOG(m_bufferingCounter >= 10 ? VB_PLAYBACK : VB_GENERAL,
LOG_NOTICE, LOC +
QString("Waited %1ms for video buffers %2")
.arg(waited_for.count()).arg(m_videoOutput->GetFrameStatus()));
m_bufferingLastMsg = QTime::currentTime();
if (waited_for > 7s && m_audio.IsBufferAlmostFull()
&& m_framesPlayed < 5
&& gCoreContext->GetBoolSetting("MusicChoiceEnabled", false))
{
m_playerFlags = static_cast<PlayerFlags>(m_playerFlags | kMusicChoice);
LOG(VB_GENERAL, LOG_NOTICE, LOC + "Music Choice program detected - disabling AV Sync.");
m_avSync.SetAVSyncMusicChoice(&m_audio);
}
msecs = 30s;
if (preBufferDebug)
msecs = 30min;
if (waited_for > msecs) // 30 seconds for internet streamed media
if (waited_for > 7s && m_audio.IsBufferAlmostFull()
&& !FlagIsSet(kMusicChoice))
{
LOG(VB_GENERAL, LOG_ERR, LOC +
"Waited too long for decoder to fill video buffers. Exiting..");
SetErrored(tr("Video frame buffering failed too many times."));
// We are likely to enter this condition
// if the audio buffer was too full during GetFrame in AVFD
LOG(VB_GENERAL, LOG_NOTICE, LOC + "Resetting audio buffer");
m_audio.Reset();
}
return false;
}

if (!m_avSync.GetAVSyncAudioPause())
m_audio.Pause(false);
SetBuffering(false);
return m_videoOutput->ValidVideoFrames();
std::chrono::milliseconds msecs { 500ms };
if (preBufferDebug)
msecs = 30min;
if ((waited_for > msecs) && !m_videoOutput->EnoughFreeFrames())
{
LOG(VB_GENERAL, LOG_NOTICE, LOC +
"Timed out waiting for frames, and"
"\n\t\t\tthere are not enough free frames. "
"Discarding buffered frames.");
// This call will result in some ugly frames, but allows us
// to recover from serious problems if frames get leaked.
DiscardVideoFrames(true, true);
}

msecs = 30s;
if (preBufferDebug)
msecs = 30min;
if (waited_for > msecs) // 30 seconds for internet streamed media
{
LOG(VB_GENERAL, LOG_ERR, LOC +
"Waited too long for decoder to fill video buffers. Exiting..");
SetErrored(tr("Video frame buffering failed too many times."));
}

return false;
}

void MythPlayer::VideoEnd(void)
Expand Down Expand Up @@ -866,8 +858,8 @@ bool MythPlayer::FastForward(float seconds)
int64_t pos = TranslatePositionMsToFrame(msec, false);
if (CalcMaxFFTime(pos) < 0)
return true;
// Reach end of recording, go to 1 or 3s before the end
dest = (m_liveTV || IsWatchingInprogress()) ? -3.0 : -1.0;
// Reach end of recording, go to offset before the end
dest = -kSeekToEndOffset;
}
uint64_t target = FindFrame(dest, true);
m_ffTime = target - m_framesPlayed;
Expand Down Expand Up @@ -1458,13 +1450,10 @@ long long MythPlayer::CalcRWTime(long long rw) const
*/
long long MythPlayer::CalcMaxFFTime(long long ffframes, bool setjump) const
{
float maxtime = 1.0;
float maxtime = kSeekToEndOffset;
bool islivetvcur = (m_liveTV && m_playerCtx->m_tvchain &&
!m_playerCtx->m_tvchain->HasNext());

if (m_liveTV || IsWatchingInprogress())
maxtime = 3.0;

long long ret = ffframes;
float ff = ComputeSecs(ffframes, true);
float secsPlayed = ComputeSecs(m_framesPlayed, true);
Expand Down
1 change: 1 addition & 0 deletions mythtv/libs/libmythtv/mythplayer.h
Expand Up @@ -243,6 +243,7 @@ class MTV_PUBLIC MythPlayer : public QObject
static const double kInaccuracyDefault;
static const double kInaccuracyEditor;
static const double kInaccuracyFull;
static const double kSeekToEndOffset;

void SaveTotalFrames(void);
void SetErrored(const QString &reason);
Expand Down
47 changes: 18 additions & 29 deletions mythtv/libs/libmythtv/mythplayeravsync.cpp
Expand Up @@ -33,38 +33,16 @@ void MythPlayerAVSync::WaitForFrame(std::chrono::microseconds FrameDue)
QThread::usleep(delay.count());
}

std::chrono::milliseconds& MythPlayerAVSync::DisplayTimecode()
void MythPlayerAVSync::ResetAVSyncForLiveTV(AudioPlayer* Audio)
{
return m_dispTimecode;
}

void MythPlayerAVSync::ResetAVSyncClockBase()
{
m_rtcBase = 0us;
}

bool MythPlayerAVSync::GetAVSyncAudioPause() const
{
return m_avsyncAudioPaused;
}

void MythPlayerAVSync::SetAVSyncAudioPause(bool Pause)
{
m_avsyncAudioPaused = Pause;
}

bool MythPlayerAVSync::ResetAVSyncForLiveTV(AudioPlayer* Audio)
{
bool result = m_rtcBase != 0us;
m_avsyncAudioPaused = kAVSyncAudioPausedLiveTV;
Audio->Pause(true);
m_avsyncAudioPaused = true;
m_rtcBase = 0us;
return result;
}

void MythPlayerAVSync::SetAVSyncMusicChoice(AudioPlayer* Audio)
{
m_avsyncAudioPaused = false;
m_avsyncAudioPaused = kAVSyncAudioNotPaused;
Audio->Pause(false);
}

Expand Down Expand Up @@ -109,6 +87,9 @@ std::chrono::microseconds MythPlayerAVSync::AVSync(AudioPlayer *Audio, MythVideo
// time weighted exponential filter coefficient
static float const s_sync_fc = 0.9F;

if (m_avsyncAudioPaused == kAVSyncAudioPausedLiveTV)
m_rtcBase = 0us;

while (framedue == 0us)
{
if (Frame)
Expand Down Expand Up @@ -177,7 +158,8 @@ std::chrono::microseconds MythPlayerAVSync::AVSync(AudioPlayer *Audio, MythVideo
// Get video in sync with audio
audio_adjustment = m_priorAudioTimecode - m_priorVideoTimecode;
// If there is excess audio - throw it away.
if (audio_adjustment < -200ms)
if (audio_adjustment < -200ms
&& m_avsyncAudioPaused != kAVSyncAudioPausedLiveTV)
{
Audio->Reset();
audio_adjustment = 0ms;
Expand Down Expand Up @@ -217,12 +199,19 @@ std::chrono::microseconds MythPlayerAVSync::AVSync(AudioPlayer *Audio, MythVideo

if (!pause_audio && m_avsyncAudioPaused)
{
m_avsyncAudioPaused = false;
Audio->Pause(false);
// If the audio was paused due to playing too close to live,
// don't unpause it until the video catches up. This helps to
// quickly achieve smooth playback.
if (m_avsyncAudioPaused != kAVSyncAudioPausedLiveTV
|| audio_adjustment < 0ms)
{
m_avsyncAudioPaused = kAVSyncAudioNotPaused;
Audio->Pause(false);
}
}
else if (pause_audio && !m_avsyncAudioPaused)
{
m_avsyncAudioPaused = true;
m_avsyncAudioPaused = kAVSyncAudioPaused;
Audio->Pause(true);
}

Expand Down
23 changes: 17 additions & 6 deletions mythtv/libs/libmythtv/mythplayeravsync.h
Expand Up @@ -10,6 +10,13 @@

class AudioPlayer;

enum AVSyncAudioPausedType
{
kAVSyncAudioNotPaused = 0,
kAVSyncAudioPaused = 1,
kAVSyncAudioPausedLiveTV = 2
};

class MythPlayerAVSync
{
public:
Expand All @@ -21,19 +28,23 @@ class MythPlayerAVSync
std::chrono::microseconds FrameInterval,
float PlaySpeed, bool HaveVideo, bool Force);
void WaitForFrame (std::chrono::microseconds FrameDue);
std::chrono::milliseconds& DisplayTimecode ();
void ResetAVSyncClockBase();
std::chrono::milliseconds& DisplayTimecode()
{ return m_dispTimecode; }
void ResetAVSyncClockBase()
{ m_rtcBase = 0us; }
void GetAVSyncData (InfoMap& Map) const;
bool GetAVSyncAudioPause () const;
void SetAVSyncAudioPause (bool Pause);
bool ResetAVSyncForLiveTV(AudioPlayer* Audio);
AVSyncAudioPausedType GetAVSyncAudioPause () const
{ return m_avsyncAudioPaused; }
void SetAVSyncAudioPause (AVSyncAudioPausedType Pause)
{ m_avsyncAudioPaused = Pause; }
void ResetAVSyncForLiveTV(AudioPlayer* Audio);
void SetAVSyncMusicChoice(AudioPlayer* Audio); // remove
void SetRefreshInterval(std::chrono::microseconds interval)
{ m_refreshInterval = interval; }

private:
QElapsedTimer m_avTimer;
bool m_avsyncAudioPaused { false };
AVSyncAudioPausedType m_avsyncAudioPaused { kAVSyncAudioNotPaused };
int m_avsyncAvg { 0 };
std::chrono::milliseconds m_dispTimecode { 0ms };
std::chrono::microseconds m_rtcBase { 0us }; // real time clock base for presentation time
Expand Down

0 comments on commit 10c2a67

Please sign in to comment.