Solved Slow Stream Stutter Question
#1
When streaming a slow stream, I fully get that it needs to buffer every once in a while. This is not a my stream is always buffering complaint.


My issue is that Kodi refuses to pause the stream to allow it to fill the buffer and instead starts doing the stuttering thing. (buffer is empty, gets a few packets, moves a few frames, buffer is empty, gets a few packets, moves a few frames, ect.)

Every once in a while it actually pauses the video, opens the buffering dialog box and buffers for a while until it can resume. <- This is the behavior I'm wanting.

I know there is plenty of buffer available. I know the buffer is active. I know the buffer is working.

so.

Any ideas of the settings I have to tweak to change the threshold so that it will truly detect a slow stream and pause it instead of letting it stutter constantly?

Edit:
I'm trying to understand the code. Not sure if I'm looking in the right place here, but..
It seems like when this is happening, it's not estimating how much buffer it needs to smoothly play the stream?
Cache Level is always 100% and Cache Delay is always 0 no matter the size of Cache Bytes.

Quote: if(m_StateInput.cache_bytes >= 0)
{
strBuf += StringUtils::Format(" forward:%s %2.0f%%"
, StringUtils::SizeToString(m_StateInput.cache_bytes).c_str()
, m_StateInput.cache_level * 100);
if(m_playSpeed == 0 || m_caching == CACHESTATE_FULL)
strBuf += StringUtils::Format(" %d sec", DVD_TIME_TO_SEC(m_StateInput.cache_delay));
}
Reply
#2
This may help HOW-TO:Modify the video cache (wiki)
Reply
#3
what is the source of the videos that are buffering?
Reply
#4
(2015-11-24, 19:26)PatK Wrote: This may help HOW-TO:Modify the video cache (wiki)

I appreciate the help, but the physical buffer itself is setup and working.
The issue mentioned here is that the stream isn't fast enough to fill the buffer in real time and that Kodi doesn't pause to let pre-buffer for smooth playback.

(2015-11-24, 19:28)bry- Wrote: what is the source of the videos that are buffering?

I'm not 100% sure what you mean by source, so I'll try to answer a few different ways.
It's happening right now, so this is almost exact what I see.

1) I'm using a add-on that finds the stream to play.

2) The stream is over the internet.

3) Audio

D(Audio: acc (LC) (mp4a / 0x6134706D, 44100 Hz, sterio, fltp, 128kb/s)
P(aq: 0%, Kb/s:###.#, att:0.0db)

Kb/s b is bouncing between 80.0 and 300.00

4)Video
D(Video: h264 (Constrained Baseline) (avc1 / 0x31637661), yuv420p, 576x432 [SAR 1:1 DAR 4:3], 563 kb/s)
P(fr23.976, vq: 0%, dc:ff-h264-dxa2, Mb/s ###.##, drop:1, Skip ####, pc:none)

Mb/s is bouncing between 0.30 and 0.60.
Skip is counting quickly and currently at 6000.

5) Other
C( ad:0.00, a/v:#.###, edl:-, dcpu: 0%, acpu: 0%, vcpu: 0%, forward:0, B 0%) <- Seems I was wrong about the 100% thingy
W( CPU0: 5%, CPU1: 3%, CPU2: 10%, CPU3: 1%, CPU4: 3%, CPU5: 5%, CPU6: 10%, CPU7 2%)

a/v is bouncing between -0.1 and .1..
Reply
#5
Seems like it's something here right?

Quote:bool CDVDPlayer::CheckStartCaching(CCurrentStream& current)
{
if(m_caching != CACHESTATE_DONE
|| m_playSpeed != DVD_PLAYSPEED_NORMAL)
return false;

if(IsInMenu())
return false;

if((current.type == STREAM_AUDIO && m_dvdPlayerAudio->IsStalled())
|| (current.type == STREAM_VIDEO && m_dvdPlayerVideo->IsStalled()))
{
if (CachePVRStream())
{
if ((current.type == STREAM_AUDIO && current.started && m_dvdPlayerAudio->GetLevel() == 0) ||
(current.type == STREAM_VIDEO && current.started && m_dvdPlayerVideo->GetLevel() == 0))
{
CLog::Log(LOGDEBUG, "%s stream stalled. start buffering", current.type == STREAM_AUDIO ? "audio" : "video");
SetCaching(CACHESTATE_PVR);
TriggerResync();
}
return true;
}

// don't start caching if it's only a single stream that has run dry
if(m_dvdPlayerAudio->GetLevel() > 50
|| m_dvdPlayerVideo->GetLevel() > 50)
return false;

if(current.inited)
SetCaching(CACHESTATE_FULL);
else
SetCaching(CACHESTATE_INIT);
return true;
}
return false;
}

The queues are obviously under the 50 percent check.

Edit: Just noticed the stalled check is a OR not an AND..

So.. Either neither stream is ever stalled, or once it puts it into CACHESTATE_FULL, something unsets it right away?
Or am I just looking at the wrong code?
Reply
#6
Sorry to keep spamming but..

I now know for a fact that it is detecting that it's stalled and setting the CACHESTATE to FULL or INIT and that it instantly detects the buffer is at 100% and resumes.

What I don't know yet is why it's thinking the buffer is 100%.
Reply
#7
by source I mean "where are you getting these video feeds from"
Reply
#8
I just gave a ton of technical info on what I believe to be a bug with Kodi, and feel like I'm zeroing on it the issue, and if I'm understanding correctly, you're wanting to know what I'm watching?

It's on any source/stream that isn't fast enough for the video.

Based on the debugging I've done so far, Kodi appears to be having trouble calculating how much buffer is needed to complete the video without running out of buffer.

I've verified this more that once in that the buffer runs empty it pauses the video. But, GetCachingTimes isn't estimating how much buffer you really need for the stream making it think the buffer is already 100% causing it instantly resume play.

I wasn't hoping someone else may have encountered this and had a fix so I didn't have to modify the code myself.
Reply
#9
This is a frustrating bug in that I don't think I can fix it completely.

I fixed GetCachingTimes where it works fine except for the very beginning of any stream.

Quote:bool CDVDPlayer::GetCachingTimes(double& level, double& delay, double& offset)
{
if(!m_pInputStream || !m_pDemuxer)
return false;

XFILE::SCacheStatus status;
if (!m_pInputStream->GetCacheStatus(&status))
return false;

cached = status.forward;
unsigned currate = status.currate;
unsigned maxrate = status.maxrate;
bool full = status.full;

The problem is that status.currate is way off when the stream begins. (This includes seeking outside the current buffer.)
For some reason currate is extremely over exaggerated on how fast the stream is really downloading for the first 5 to 10 seconds.

The video will stop when the Audio or Video buffer empties but instantly resumes because it doesn't think it needs a buffer.

This means my fix can still stutter if the buffer runs out within the first 5-10 seconds but then works as intended later. Sad

Does ANYONE know why currate is so wrong when you start a new stream?
Reply
#10
Welp, here's what I'm using and seems to do the job for better or worse..

If the stream is slow, it will start buffering right away.
If the stream is fast, it will start playing right away.

No more stuttering, YAY!

Note: This has my debug still in it..

Quote:Code removed. New code listed below.
Reply
#11
I spent some time and improved from my original fix as the previous one just tried to Band-Aid over another issues.

  1. Cache rate average is now a rolling window (Who cares what bandwidth you had a hour ago when predicting how fast it is now)
  2. Filter out the strange data when cache is first started.
  3. Reworked the logic to determine the amount of needed buffer.

So far this has worked great for slow streams, fast streams, and fast streams that turn slow much later.
I'm sure there are corner cases that I don't know about, but this is working great for our needs.

If someone has some pull with the devs, please have them at least look at this as the original code is definitely not right and this is at least something to start from for a fix.

Quote: xbmc/filesystem/FileCache.cpp | 38 ++++++++++++++++++++++++++++----------
1 file changed, 28 insertions(+), 10 deletions(-)

diff --git a/xbmc/filesystem/FileCache.cpp b/xbmc/filesystem/FileCache.cpp
index 2d76b65..e7a6608 100644
--- a/xbmc/filesystem/FileCache.cpp
+++ b/xbmc/filesystem/FileCache.cpp
@@ -65,19 +65,37 @@ public:
}
}

- unsigned Rate(int64_t pos, unsigned int time_bias = 0)
+ unsigned Rate(int64_t pos, unsigned int time_bias = 0, unsigned int TimeRange = 30000)
{
const unsigned ts = XbmcThreads::SystemClockMillis();

- m_size += (pos - m_pos);
- m_time += (ts - m_stamp);
- m_pos = pos;
- m_stamp = ts;
-
- if (m_time == 0)
- return 0;
+ if (m_time > TimeRange)
+ {
+ m_size *= 0.85;
+ m_time *= 0.85;
+ }

- return (unsigned)(1000 * (m_size / (m_time + time_bias)));
+ m_time += (ts - m_stamp);
+ m_size += (pos - m_pos);
+ m_pos = pos;
+ m_stamp = ts;
+
+ if (m_time == 0 || m_time < TimeRange * 0.25)
+ {
+ m_size = 0;
+ return 0;
+ }
+
+#define StartupBuffer (TimeRange * 0.75)
+
+ if (m_time < StartupBuffer)
+ {
+ return (unsigned)(1000 * (m_size / (StartupBuffer + time_bias)));
+ }
+ else
+ {
+ return (unsigned)(1000 * (m_size / (m_time + time_bias)));
+ }
}

void Pause()
@@ -387,7 +405,7 @@ void CFileCache:Tonguerocess()

// under estimate write rate by a second, to
// avoid uncertainty at start of caching
- m_writeRateActual = average.Rate(m_writePos, 1000);
+ m_writeRateActual = average.Rate(m_writePos);
}
}

Quote: xbmc/cores/dvdplayer/DVDPlayer.cpp | 81 ++++++++++++++++++++++++++++----------
1 file changed, 60 insertions(+), 21 deletions(-)

diff --git a/xbmc/cores/dvdplayer/DVDPlayer.cpp b/xbmc/cores/dvdplayer/DVDPlayer.cpp
index 51cce98..5000438 100644
--- a/xbmc/cores/dvdplayer/DVDPlayer.cpp
+++ b/xbmc/cores/dvdplayer/DVDPlayer.cpp
@@ -1688,6 +1688,13 @@ void CDVDPlayer:TonguerocessRadioRDSData(CDemuxStream* pStream, DemuxPacket* pPacket
m_dvdPlayerRadioRDS->SendMessage(new CDVDMsgDemuxerPacket(pPacket, drop));
}

+static double play_sbp = 0;
+static double cache_sbp = 0;
+static double play_left = 0;
+static double cache_left = 0;
+static double cache_need = 0;
+static double cached = 0;
+
bool CDVDPlayer::GetCachingTimes(double& level, double& delay, double& offset)
{
if(!m_pInputStream || !m_pDemuxer)
@@ -1697,41 +1704,66 @@ bool CDVDPlayer::GetCachingTimes(double& level, double& delay, double& offset)
if (!m_pInputStream->GetCacheStatus(&status))
return false;

- int64_t cached = status.forward;
+ cached = status.forward;
unsigned currate = status.currate;
unsigned maxrate = status.maxrate;
bool full = status.full;

- int64_t length = m_pInputStream->GetLength();
- int64_t remain = length - m_pInputStream->Seek(0, SEEK_CUR);
+ double length = (double)(m_pInputStream->GetLength());
+ double remain = length - (double)(m_pInputStream->Seek(0, SEEK_CUR));

if(cached < 0 || length <= 0 || remain < 0)
return false;

- double play_sbp = DVD_MSEC_TO_TIME(m_pDemuxer->GetStreamLength()) / length;
+ play_sbp = DVD_MSEC_TO_TIME(m_pDemuxer->GetStreamLength()) / length;
double queued = 1000.0 * GetQueueTime() / play_sbp;

delay = 0.0;
level = 0.0;
offset = (double)(cached + queued) / length;

- if (currate == 0)
- return true;
-
- double cache_sbp = 1.1 * (double)DVD_TIME_BASE / currate; /* underestimate by 10 % */
- double play_left = play_sbp * (remain + queued); /* time to play out all remaining bytes */
- double cache_left = cache_sbp * (remain - cached); /* time to cache the remaining bytes */
- double cache_need = std::max(0.0, remain - play_left / cache_sbp); /* bytes needed until play_left == cache_left */
+ if (currate)
+ {
+ cache_sbp = 1.1 * (double)(DVD_TIME_BASE) / (double)(currate); /* underestimate by 10 % */
+ }
+ else
+ {
+ if (full)
+ {
+ cache_sbp = play_sbp;
+ }
+ else
+ {
+ cache_sbp = 0;
+ }
+ }

- delay = cache_left - play_left;
+ play_left = play_sbp * (remain + queued); /* time to play out all remaining bytes */
+ cache_left = cache_sbp * (remain - queued - cached); /* time to cache the remaining bytes */
+ delay = cache_left - play_left;
+ if (cache_sbp)
+ {
+ cache_need = std::max(0.0, (delay) / cache_sbp); /* bytes needed until play_left == cache_left */
+ }
+ else
+ {
+ cache_need = remain - queued;
+ }
+
+ if (cache_need)
+ {
+ level = cached / cache_need;
+ }
+ else
+ {
+ level = 1.0;
+ }

- if (full && (currate < maxrate) )
+ if (full && level < 1.0)
{
CLog::Log(LOGDEBUG, "Readrate %u is too low with %u required", currate, maxrate);
level = -1.0; /* buffer is full & our read rate is too low */
}
- else
- level = (cached + queued) / (cache_need + queued);

return true;
}
@@ -1752,16 +1784,16 @@ void CDVDPlayer::HandlePlaySpeed()
if(level < 0.0)
{
CGUIDialogKaiToast::QueueNotification(g_localizeStrings.Get(21454), g_localizeStrings.Get(21455));
- caching = CACHESTATE_INIT;
+ caching = CACHESTATE_DONE;
}
if(level >= 1.0)
- caching = CACHESTATE_INIT;
+ caching = CACHESTATE_DONE;
}
else
{
if ((!m_dvdPlayerAudio->AcceptsData() && m_CurrentAudio.id >= 0)
|| (!m_dvdPlayerVideo->AcceptsData() && m_CurrentVideo.id >= 0))
- caching = CACHESTATE_INIT;
+ caching = CACHESTATE_DONE;
}
}

@@ -1770,7 +1802,7 @@ void CDVDPlayer::HandlePlaySpeed()
// if all enabled streams have been inited we are done
if((m_CurrentVideo.id < 0 || m_CurrentVideo.started)
&& (m_CurrentAudio.id < 0 || m_CurrentAudio.started))
- caching = CACHESTATE_PLAY;
+ caching = CACHESTATE_FULL;

// handle situation that we get no data on one stream
if(m_CurrentAudio.id >= 0 && m_CurrentVideo.id >= 0)
@@ -3103,14 +3135,21 @@ void CDVDPlayer::GetGeneralInfo(std:Confusedtring& strGeneralInfo)
strBuf += StringUtils::Format(" %d sec", DVD_TIME_TO_SEC(m_StateInput.cache_delay));
}

- strGeneralInfo = StringUtils::Format("C( ad:% 6.3f, a/v:% 6.3f%s, dcpu:%2i%% acpu:%2i%% vcpu:%2i%%%s )"
+ strGeneralInfo = StringUtils::Format("C( ad:% 6.3f, a/v:% 6.3f%s, dcpu:%2i%% acpu:%2i%% vcpu:%2i%%%s )\nD( Psbp:% 6.3f, Csbp:% 6.3f, PL:% 6.3f, CL:% 6.3f, CN:% 6.3f, C:% 6.3f, Diff:% 6.3f)"
, dDelay
, dDiff
, strEDL.c_str()
, (int)(CThread::GetRelativeUsage()*100)
, (int)(m_dvdPlayerAudio->GetRelativeUsage()*100)
, (int)(m_dvdPlayerVideo->GetRelativeUsage()*100)
- , strBuf.c_str());
+ , strBuf.c_str()
+ , play_sbp
+ , cache_sbp
+ , play_left
+ , cache_left
+ , cache_need
+ , cached
+ , cache_need - cached);
}
}
}
Reply
#12
why don't you try and submit your code on the github site?
Reply
#13
(2015-11-29, 01:25)helta Wrote: why don't you try and submit your code on the github site?

A few reasons.

I honestly don't understand git. I'm used to subversion.
I'm surprised I was even able to download the code to be honest.

This is not a fully 100% solution but more of a prototype to show a possible solution.
Although this code is less broke than the original, it needs additional tweaks to get it right.
I personally don't have the time to finalize it.

For example, it's clear to me now that the CacheLeft or PlayLeft is a little off. So.. It works great at the beginning, but as you get closer to the end of the video, the CacheNeeded seems to be too small. If I find time, I may look into why, but since it no longer stutters and may buffer a couple more times than it should on a rare occasion, I can live with it.
Reply
#14
Did this suggested fix or something based on it ever make into the Kodi official code? This is still a problem in the latest.

Of course, it may not be a high priority item for the Kodi if they believe that these problems are primarily with streaming copyrighted content illegally that they are very sensitive about which is why I believe the question above on what source is being used. :-)
Reply
#15
Is there anything being done about implementing this fix for a known problem? The work seems to have already been done, just needs to be copied and pasted into the right spot.
Reply

Logout Mark Read Team Forum Stats Members Help
Slow Stream Stutter Question0