Permalink
Browse files

Byte based seeking support for MPEG files

This helps to resolve a long standing issue where files that have bad PTS timelines
cause the playback time to jump wildly and infuriate you when you try
to skip around them. :)

What it does is detect when the parser's calculated duration for a file that is
being played back (extender only, no transcoding) is different by a decent amount (25%)
from that of what SageTV thinks the recorded duration should be. Then it uses the read
file position to estimate the current playback time and then for seeking, it guesses
the proper file position as if the file were constant bitrate. This works WAY better
than being stuck without the ability to seek or even know what time you are at in the file.

This feature is off by default, and it can be enabled by setting this in your Sage.properties file:
disable_byte_based_seek_check=false

This does NOT work on files that are currently recording.
This does NOT allow you to fast forward or rewind properly in files like this, only seek.
You CAN force this to work for currently recording files by setting this property in the
properties file for the extender you want it to work on:
force_byte_based_seeking=true
(this is so if you're recording something you really want to watch right now that has
this issue, you can turn off the extender, edit the properties file and then it'll make it
happen...don't forget to reset that property later).
This is somewhat inaccurate, but that's what you get with byte-based time estimation.
  • Loading branch information...
Narflex committed Jan 10, 2018
1 parent fe61793 commit a6b621973d06fb14ee23433782b7f2671d4da6f0
Showing with 68 additions and 6 deletions.
  1. +9 −0 java/sage/FastMpeg2Reader.java
  2. +10 −0 java/sage/MediaFile.java
  3. +49 −6 java/sage/MiniPlayer.java
@@ -1656,6 +1656,15 @@ public void seekToBeginning() throws IOException
msecStartForSkip = lastRawIFramePTS = lastRawVideoPTS = 0;
lastFullParseBytePos = -(DIST_BETWEEN_FULL_PARSES + 1);
}
public void seekToPosition(long pos) throws IOException {
if (pos <= 0) {
seekToBeginning();
} else {
ins.position(pos);
bitsDone = pos*8;
}
}
public void seek(long seekTime) throws IOException
{
View
@@ -3882,6 +3882,16 @@ public synchronized long getDuration(int segment)
{
return getEnd(segment) - getStart(segment);
}
public synchronized long getDuration(java.io.File file) {
if (file == null) return -1;
for (int i = 0; i < files.size(); i++) {
if (files.get(i).equals(file)) {
return getDuration(i);
}
}
return -1;
}
public synchronized int segmentLocation(long segTime, boolean roundForward)
{
View
@@ -156,6 +156,7 @@ else if (rpSrc != null)
else
{
mpegSrc.init(true, !timeshifted, usingRemuxer);
checkForByteBasedSeeking(file);
}
}
catch (java.io.IOException e)
@@ -397,7 +398,7 @@ else if (rpSrc != null)
}
else
{
duration = (mySrc == null || timeshifted) ? 0 : mySrc.getDurationMillis();
duration = (mySrc == null || timeshifted || byteBasedSeeking) ? 0 : mySrc.getDurationMillis();
}
if (Sage.DBG) System.out.println("getDuration : "+ duration);
return duration;
@@ -458,7 +459,7 @@ private long getNativeMediaTimeNoSync()
}
else
{
long otherTime;
long otherTime = 0;
if(transcoded)
{
otherTime = (tcSrc != null ? tcSrc.getFirstTimestampMillis() : 0);
@@ -475,7 +476,7 @@ else if (rpSrc != null)
otherTime = otherTime - FastMpeg2Reader.MAX_PTS/90;
}
}
else
else if (!byteBasedSeeking)
{
otherTime = (mpegSrc != null ? mpegSrc.getFirstTimestampMillis() : 0);
if (otherTime - nativeTime > 100000 && mpegSrc != null && mpegSrc.didPTSRollover())
@@ -506,7 +507,7 @@ else if (rpSrc != null)
return rv;
}
}
public boolean getMute()
{
return currMute;
@@ -588,6 +589,33 @@ else if (rpSrc != null && timeshifted)
}
timeshifted = serverSideTranscoding;
}
void checkForByteBasedSeeking(java.io.File file) {
if (Sage.getBoolean("disable_byte_based_seek_check", true)) return;
if (mpegSrc != null) {
if (uiMgr.getBoolean("force_byte_based_seeking", false)) {
System.out.println("Forcing byte based seeking due to property setting");
byteBasedSeeking = true;
} else if (!timeshifted) {
// We can't check duration for files that are currently recording because
// the parser doesn't check the duration in that case and it's problematic
// if we change that.
long parserDuration = mpegSrc.getDurationMillis();
if (parserDuration < 0) {
System.out.println("Using byte based seeking due to invalid duration from parser");
byteBasedSeeking = true;
} else {
long fileDur = VideoFrame.getMediaFileForPlayer(this).getDuration(file);
long diff = Math.abs(parserDuration - fileDur);
if (fileDur > Sage.MILLIS_PER_MIN && diff > fileDur/4) {
byteBasedSeeking = true;
System.out.println("Using byte based seeking due to duration mismatch between " +
"parser (" + parserDuration + ") and recording (" + fileDur + ")");
}
}
}
}
}
public void load(byte majorTypeHint, byte minorTypeHint, String encodingHint, java.io.File file, String hostname, boolean timeshifted, long bufferSize) throws PlaybackException
{
@@ -1135,6 +1163,7 @@ else if (currFileFormat != null && Sage.getBoolean("miniplayer/align_iframes_on_
else
{
mpegSrc.init(true, !timeshifted, usingRemuxer);
checkForByteBasedSeeking(file);
}
}
catch (java.io.IOException e)
@@ -2210,10 +2239,15 @@ else if (rpSrc != null)
}
else
{
if(transcoded)
if (byteBasedSeeking) {
long target = Math.round((((double) seekTimeMillis) /
VideoFrame.getMediaFileForPlayer(this).getDuration(currFile)) * mpegSrc.length());
mpegSrc.seekToPosition(target);
} else if(transcoded) {
tcSrc.seek(seekTimeMillis);
else
} else {
mpegSrc.seek(seekTimeMillis);
}
if (currState == PAUSE_STATE && (mcsr != null && mcsr.supportsFrameStep()))
{
@@ -3031,6 +3065,14 @@ protected long getMediaTimeMillis0()
lastMediaTimeCacheTime = Sage.eventTime();
return lastMediaTime;
}
if (byteBasedSeeking && mpegSrc != null) {
// Generate an estimated media time based off the byte position and
// the current known duration of the file.
long currPos = Math.max(0, mpegSrc.getReadPos() - maxAvailBufferSize);
double relativePos = ((double) currPos) / mpegSrc.length();
MediaFile mf = VideoFrame.getMediaFileForPlayer(this);
currMediaTime = Math.round(mf.getDuration(currFile) * relativePos);
}
// I don't see any good reason to do this interpolation on embedded since if we sent a request to the miniclient above
// then we should have a pretty accurate time counter right now
if (lastMediaTimeBase == currMediaTime && currState == PLAY_STATE)
@@ -4078,6 +4120,7 @@ private synchronized void checkLazies() throws java.io.IOException
protected long timeGuessMillis;
protected long guessTimestamp;
protected long timestampOffset;
protected boolean byteBasedSeeking;
protected boolean serverSideTranscoding;
protected boolean pushMode;

0 comments on commit a6b6219

Please sign in to comment.