From 9b7168b65bc65948edd542d76b1f1c803b00a584 Mon Sep 17 00:00:00 2001 From: larena1 Date: Sat, 25 Jul 2026 02:01:12 +0200 Subject: [PATCH 1/6] [AdaptiveStream] Deliver a partial read instead of discarding the segment tail read() advanced segment_read_pos_ and absolute_position_ by the number of bytes available, but only copied them out and reported them when they happened to satisfy the full requested amount. Whenever a read straddled the end of a segment buffer - which happens at every segment boundary - the remaining bytes were skipped over without ever being delivered, and the caller was told the read failed. For the TS demuxer that error turns into AVCONTEXT_IO_ERROR, and TSReader::ReadPacket recovers from it by calling Reset(), whose Tell() runs read(0, 0) and therefore ensureSegment(), advancing to the next segment. The parser re-anchors there and everything it had not yet emitted from the current segment is lost - a hole of several seconds in the delivered video, at segment boundaries during plain playback as well as after a seek. Copy and report what is available. ReadPartial is a partial read by contract and AP4_ByteStream::Read loops for the remainder, so a request spanning a segment boundary is now satisfied across both segments instead of failing. Also guard the unsigned subtraction that computes the available count: a read position past the downloaded end would wrap to a huge value and read beyond the buffer. Equality is legitimate - nothing is available yet and the wait below may still deliver data - so only a position strictly beyond it is rejected. Assisted-by: Claude Opus 5 --- src/common/AdaptiveStream.cpp | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/common/AdaptiveStream.cpp b/src/common/AdaptiveStream.cpp index 05b1e6476..6e9b526ab 100644 --- a/src/common/AdaptiveStream.cpp +++ b/src/common/AdaptiveStream.cpp @@ -1001,6 +1001,12 @@ uint32_t adaptive::AdaptiveStream::read(void* buffer, uint32_t bytesToRead) SegmentBuffer& currSegBuffer = m_segBuffers.Front(); + // The subtraction below is unsigned: a read position past the downloaded end would wrap to a + // huge count and read beyond the buffer. Equality is legitimate (nothing available yet, the + // wait below may still deliver data), only a position beyond it is not. + if (segment_read_pos_ > currSegBuffer.BufferSize()) + return 0; + size_t avail = currSegBuffer.BufferSize() - segment_read_pos_; { @@ -1019,14 +1025,20 @@ uint32_t adaptive::AdaptiveStream::read(void* buffer, uint32_t bytesToRead) if (avail > bytesToRead) avail = bytesToRead; + if (avail == 0) + return 0; + + // Deliver what is available rather than requiring the full amount: this is a partial read and + // the caller is expected to ask again for the remainder. Advancing the positions for bytes that + // are then neither copied nor reported discards the tail of every segment - and the read error + // it produces sends the caller into its IO error recovery, which re-anchors on the *next* + // segment and drops everything the demuxer had not yet emitted from the current one. + currSegBuffer.CopyBufferTo(buffer, segment_read_pos_, avail); + segment_read_pos_ += avail; absolute_position_ += avail; - if (avail == bytesToRead) - { - currSegBuffer.CopyBufferTo(buffer, segment_read_pos_ - avail, avail); - return static_cast(avail); - } + return static_cast(avail); } return 0; From 0756344c601166ec3edb22ebaa0929710ad4b9cb Mon Sep 17 00:00:00 2001 From: larena1 Date: Sat, 25 Jul 2026 02:01:12 +0200 Subject: [PATCH 2/6] [AdaptiveStream] Do not drop a segment the reader has only caught up with ensureSegment() treats "read position has reached BufferSize()" as "segment fully read". BufferSize() is however the amount downloaded so far, not the size of the segment. When the reader catches up with an ongoing download - at startup, and after every seek, where the download has no head start - the read position reaches that end long before the segment is complete, and the segment is popped with everything that had not yet arrived. Nothing fails visibly at that point: ResetSegment() sets segment_read_pos_ to 0 and leaves absolute_position_ alone for TS, so the absolute position maps cleanly onto the following segment and the demuxer simply reads on there. The result is a hole of several seconds in the delivered media with no read error, no failed seek and no IO error anywhere - which starves Kodi's video player into a stillframe and a decoder reset while audio keeps playing. The existing lock_guard on mutexWorker was aimed at this case but only waits for the mutex, not for data; read() has the correct wait but runs after this decision has already been made. Wait on the same condition variable read() uses until the download delivers more data or leaves the QUEUED/DOWNLOADING state, then re-check before popping. The wait is polled and bounded rather than indefinite: the download thread changes the buffer state and notifies cvRW without holding mutexRW, so a notification issued between the predicate check and the wait is lost and the waiter would never wake, and downloads can also be paused, in which case no notification is coming at all. This was observed on a stream with small, quickly downloaded segments, where the segment reached DOWNLOADED one millisecond after the reader caught up and the demuxer thread blocked - which also made the player impossible to stop. Re-evaluating on a timeout covers both cases, and on expiry the code falls through to the previous behaviour, so this can degrade but never hang. Verified with instrumentation on the affected stream: the condition fires reliably at every download chunk boundary (segment_read_pos_ 1540096, 3080192, ... = multiples of 8192 TS packets), and with the wait in place seven consecutive seeks produced no gap in the packets delivered to Kodi, where every previous build produced one within seconds of the seek and at playback start. Assisted-by: Claude Opus 5 --- src/common/AdaptiveStream.cpp | 38 +++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/common/AdaptiveStream.cpp b/src/common/AdaptiveStream.cpp index 6e9b526ab..214307c2e 100644 --- a/src/common/AdaptiveStream.cpp +++ b/src/common/AdaptiveStream.cpp @@ -776,6 +776,44 @@ bool adaptive::AdaptiveStream::ensureSegment() if (m_segBuffers.IsEmpty() || (m_segBuffers.Front().BufferSize() != 0 && segment_read_pos_ >= m_segBuffers.Front().BufferSize())) { + // BufferSize() is how much of the segment has been downloaded so far, not how large the segment + // is. When the reader catches up with an ongoing download the read position reaches that end + // while the segment is far from complete - dropping it here discards everything that had not + // arrived yet, and reading silently continues in the *next* segment: the delivered stream loses + // seconds of media without any read or seek ever failing. Wait for the download to deliver more + // data (or to finish) before concluding that the segment has been consumed. + if (!m_segBuffers.IsEmpty()) + { + // Poll rather than wait indefinitely. The download thread changes the buffer state and + // notifies without holding mutexRW, so a notification issued between our predicate check and + // the wait is lost and we would never wake up - and downloads can also be paused, in which + // case no notification is coming at all. Re-evaluating on a timeout covers both, and the + // overall bound guarantees that this can never hang the demuxer thread: on expiry we fall + // through to the previous behaviour rather than block playback. + constexpr auto pollInterval{std::chrono::milliseconds(50)}; + constexpr auto maxWait{std::chrono::seconds(5)}; + + std::unique_lock lckrw(thread_data_->mutexRW); + const SegmentBuffer& frontBuffer = m_segBuffers.Front(); + const auto waitUntil{std::chrono::steady_clock::now() + maxWait}; + + while (segment_read_pos_ >= frontBuffer.BufferSize() && + (frontBuffer.State() == BufferState::QUEUED || + frontBuffer.State() == BufferState::DOWNLOADING) && + thread_data_->State() == THREADDATA::ThState::RUNNING && + std::chrono::steady_clock::now() < waitUntil) + { + thread_data_->cvRW.wait_for(lckrw, pollInterval); + } + } + + // Re-check: the wait above may have made more data available, so the segment is not consumed + if (!m_segBuffers.IsEmpty() && m_segBuffers.Front().BufferSize() != 0 && + segment_read_pos_ < m_segBuffers.Front().BufferSize()) + { + return true; + } + if (!m_segBuffers.IsEmpty() && m_segBuffers.Front().State() == BufferState::DOWNLOADING) { // Although the reading position has reached the end, the segment status may not yet be updated From e69963417936b214420f8a2834d4efce175473a6 Mon Sep 17 00:00:00 2001 From: larena1 Date: Sat, 25 Jul 2026 02:01:13 +0200 Subject: [PATCH 3/6] [AdaptiveStream][TSReader] Fail a seek before the buffered segment instead of wrapping Positions passed to seek() are absolute over the whole stream while only the current segment is buffered. A position before that segment underflowed the unsigned subtraction that maps it into the segment buffer, producing a huge offset that the following clamp turned into "end of the current segment" - and the method then returned false having already moved the read position. TSReader::ReadAV in turn discarded the result of Seek() and read regardless. So the parser was handed valid-looking data from an entirely different point in the timeline while being told the read succeeded, then continued in the *next* segment, silently dropping everything in between. Reject the out-of-range position up front so the failure is explicit and the read position stays untouched, and propagate it in ReadAV so the demuxer sees an IO error instead of wrong data. Assisted-by: Claude Opus 5 --- src/common/AdaptiveStream.cpp | 10 +++++++++- src/demuxers/TSReader.cpp | 9 ++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/common/AdaptiveStream.cpp b/src/common/AdaptiveStream.cpp index 214307c2e..04a5d40a6 100644 --- a/src/common/AdaptiveStream.cpp +++ b/src/common/AdaptiveStream.cpp @@ -1189,7 +1189,15 @@ bool adaptive::AdaptiveStream::seek(uint64_t const pos, bool& isEos) } } - segment_read_pos_ = static_cast(pos - (absolute_position_ - segment_read_pos_)); + // Positions are absolute over the whole stream while only the current segment is buffered, so a + // position before that segment cannot be served. Reject it explicitly: the subtraction below is + // unsigned, so letting it through wraps around to a huge offset that the clamp then turns into + // "end of the current segment" - a silent jump to a completely different point in the timeline. + const uint64_t segStartPos{absolute_position_ - segment_read_pos_}; + if (pos < segStartPos) + return false; + + segment_read_pos_ = static_cast(pos - segStartPos); if (segment_read_pos_ > currSegBuffer.BufferSize()) { diff --git a/src/demuxers/TSReader.cpp b/src/demuxers/TSReader.cpp index d5adeb317..b2b663984 100644 --- a/src/demuxers/TSReader.cpp +++ b/src/demuxers/TSReader.cpp @@ -77,7 +77,14 @@ TSReader::~TSReader() bool TSReader::ReadAV(uint64_t pos, unsigned char * data, size_t len) { - m_stream->Seek(pos); + // The seek result must be honoured: a position that is no longer reachable (before the start of + // the segment currently buffered) leaves the stream clamped to the end of that segment. Reading + // there and reporting success hands the demuxer data from an entirely different point in the + // timeline than the position it asked for - the parser then continues in the *next* segment and + // the delivered stream loses everything in between. + if (AP4_FAILED(m_stream->Seek(pos))) + return false; + return AP4_SUCCEEDED(m_stream->Read(data, static_cast(len))); } From d54447d209504075318b642b76830e340eff7a17 Mon Sep 17 00:00:00 2001 From: larena1 Date: Sat, 25 Jul 2026 02:01:13 +0200 Subject: [PATCH 4/6] [TSReader] Land TS seeks on the segment-start keyframe Two problems on MPEG-TS seeks: 1. Latency: the seek scanned forward for a keyframe at/after the target. The MPEG-TS parser reads ahead, so on long-GOP content the next recognised keyframe was at the following segment boundary - the scan downloaded the whole current segment (seconds of latency) and overshot the requested time. 2. A/V desync: because of the same read-ahead the reported PTS did not match the frame actually delivered, so CSession::SeekTime aligned the audio streams to the wrong PTS and audio started ahead of the picture. Stop at the first recovery point (keyframe) instead, which is the one that starts the segment AdaptiveStream::seek_time already selected as containing the requested time. This content carries a single keyframe per segment, at its start (verified on a sample segment: one random_access_indicator, at the first video packet, followed by a 250 frame / 10 second GOP), so the previous scan could not land inside the current segment at all. The scan is still bounded by timeInTs, so a stream that never flags a recovery point degrades to the previous behaviour instead of scanning to EOS. Do not reposition to the recovery position afterwards. The packet we want is already read and CTSSampleReader::TimeSeek hands this m_pkt to Kodi as the first sample, while the packets that follow it are queued in the elementary stream buffers and are drained in order. Seeking the AVContext back to GetRecoveryPos() instead loses them: the parser read-ahead puts that position *behind* the packet just delivered, and reading resumed a full GOP later - the video stream got a hole of seconds right after every seek while audio kept feeding, which starves Kodi's video player into a stillframe and a decoder reset. The reported PTS now matches what is delivered (A/V stays aligned), and no extra segment is downloaded. For scrubber (accurate) seeks Kodi drops the decoded frames up to the requested time, landing exactly; skip seeks land on the segment-start keyframe. Audio-only TS keeps the forward scan (no keyframes to snap to). Assisted-by: Claude Opus 5 --- src/demuxers/TSReader.cpp | 43 ++++++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/src/demuxers/TSReader.cpp b/src/demuxers/TSReader.cpp index b2b663984..6c0f1aaa2 100644 --- a/src/demuxers/TSReader.cpp +++ b/src/demuxers/TSReader.cpp @@ -224,21 +224,44 @@ bool TSReader::SeekTime(uint64_t timeInTs) break; } - uint64_t lastRecovery(static_cast(m_startPos)); - while (m_pkt.pts == PTS_UNSET || static_cast(m_pkt.pts) < timeInTs) + if (hasVideo) { - uint64_t thisFrameStart(m_AVContext->GetRecoveryPos()); - if (!ReadPacket()) - return false; - if (!hasVideo || m_pkt.recoveryPoint || thisFrameStart >= m_startPos) + // Stop at the first recovery point (keyframe), which is the one that starts the segment + // AdaptiveStream::seek_time already selected as containing the requested time. + // + // Scanning on to the first keyframe at/after timeInTs - the previous behaviour - cannot land + // inside the current segment at all: this content carries a single keyframe per segment, at its + // start (verified on a sample segment: one random_access_indicator, at the first video packet, + // followed by a 250 frame / 10 second GOP). The scan therefore always ran into the *next* + // segment, downloading the whole remainder of the current one and overshooting the requested + // time by up to a full segment. + // + // Bounded by timeInTs so a stream that never flags a recovery point degrades to the previous + // behaviour instead of scanning to EOS. + while (m_pkt.pts == PTS_UNSET || + (!m_pkt.recoveryPoint && static_cast(m_pkt.pts) < timeInTs)) { - lastRecovery = thisFrameStart; - if (static_cast(m_pkt.pts) >= timeInTs) - break; + if (!ReadPacket()) + return false; + } + } + else + { + // Audio-only TS has no keyframes, so land as close to the requested time as possible. + while (m_pkt.pts == PTS_UNSET || static_cast(m_pkt.pts) < timeInTs) + { + if (!ReadPacket()) + return false; } } - m_AVContext->GoPosition(lastRecovery, true); + // Do not reposition to the recovery position afterwards. The packet we want is already read and + // CTSSampleReader::TimeSeek hands this m_pkt to Kodi as the first sample, while the packets that + // follow it are queued in the elementary stream buffers and are drained in order. Seeking the + // AVContext back to GetRecoveryPos() instead loses them: the MPEG-TS parser reads ahead, so that + // position lies *behind* the packet just delivered, and reading resumed a full GOP later - the + // video stream got a hole of seconds right after every seek while audio kept feeding, which + // starves Kodi's video player into a stillframe and a decoder reset. return true; } From b75b9dc7c485cf063a000d893b5f703df6a516ca Mon Sep 17 00:00:00 2001 From: larena1 Date: Sat, 25 Jul 2026 02:01:13 +0200 Subject: [PATCH 5/6] [Segment] Select the segment starting at an exact PTS boundary CSegContainer::FindByPTSOrNext treated m_endPts as inclusive, but it is the exclusive end of a segment - for a contiguous timeline it equals the start PTS of the next segment. A pts landing exactly on a boundary therefore matched the segment that *ends* there and the search returned the previous segment. This is hit on every seek: CSession::SeekTime replaces the requested time with the PTS of the video sample actually found and aligns the audio streams to it. Since the video seek lands on a segment-start keyframe, that PTS is exactly a segment boundary, so the audio stream selected the preceding segment and started up to a full segment ahead of the picture. Use the half-open range [startPTS_, m_endPts). A pts at the very end of the last segment still resolves to that segment, so seeking to the stream end is unchanged. Assisted-by: Claude Opus 5 --- src/common/Segment.cpp | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/common/Segment.cpp b/src/common/Segment.cpp index 6f00790db..5559093b1 100644 --- a/src/common/Segment.cpp +++ b/src/common/Segment.cpp @@ -140,12 +140,23 @@ const CSegment* PLAYLIST::CSegContainer::FindByPTSOrNext(uint64_t pts) const { for (const CSegment& seg : m_segments) { - if (seg.startPTS_ <= pts && pts <= seg.m_endPts) + // m_endPts is the exclusive end of the segment (== start of the next one for + // contiguous timelines). Use a half-open range [startPTS_, m_endPts) so that a + // pts landing exactly on a segment boundary selects the segment that *starts* + // there, not the one that ends there. Otherwise aligning audio to a video seek + // that lands on a segment-start keyframe (an exact boundary) picks the previous + // audio segment, starting audio a full segment ahead of the picture. + if (seg.startPTS_ <= pts && pts < seg.m_endPts) return &seg; if (seg.startPTS_ > pts) return &seg; } + + // pts is at/after the end of the last segment: fall back to it (seek to stream end) + if (!m_segments.empty() && pts == m_segments.back().m_endPts) + return &m_segments.back(); + return nullptr; } From a16fa1af26152dab4f321331d6eb2f603d7a2f40 Mon Sep 17 00:00:00 2001 From: larena1 Date: Sat, 25 Jul 2026 02:01:13 +0200 Subject: [PATCH 6/6] [Seek] Co-time audio to the delivered video PTS After a seek the video lands on its segment-start sync sample, but each audio representation was still positioned through its own manifest timing plus the frozen per-stream PTS diff (CSession::SeekTime -> ISampleReader::TimeSeek). Deep into a recording the audio and video timelines drift apart by a fixed offset, so the audio reader emitted a PTS ~1.8s away from the video sample. Kodi's VideoPlayer synchronises on the emitted PTS, not on the addon's internal elapsed time, so the picture started seconds before the sound. This is hit on every skip seek and, identically, on resume-from-position (Kodi issues a seek to the resume point right at startup). Observed on fMP4 (DASH/Smooth) and TS. Audio and video are delivered from the same source PTS clock. Once the video sample actually delivered is known, align the audio reader straight to that reader PTS instead of routing it through the manifest offset: - ISampleReader::TimeSeekReaderPts(pts): seek to an absolute reader PTS (the domain PTS() returns), i.e. without the TimeSeek() m_ptsDiff compensation. Implemented once on the interface by removing that compensation before delegating to TimeSeek(), which every reader already applies. - CSession::SeekTime remembers the video reader PTS and co-times the audio streams that own a segment buffer to it. SeekAdStream has already reset the audio segment to its start, so the seek lands on the target whether or not the reader was running yet - this also covers the resume seek that happens before the first DemuxRead starts the readers. Segment selection is unchanged (it already uses the common elapsed time); only the in-segment reader landing is corrected. fMP4 audio muxed into the video stream (no own segment buffer, hasAdStream == false) keeps its existing behaviour. A LOGINFO/LOGWARNING line reports the residual A/V delta after alignment, so a bad landing (target outside the selected audio segment) is visible in the log rather than only audible. Assisted-by: Claude Opus 5 --- src/Session.cpp | 63 ++++++++++++++++++++++++++++++++- src/samplereader/SampleReader.h | 18 ++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/src/Session.cpp b/src/Session.cpp index afbbb4bf9..ab8e2de2e 100644 --- a/src/Session.cpp +++ b/src/Session.cpp @@ -28,6 +28,7 @@ #include #include +#include using namespace adaptive; using namespace PLAYLIST; @@ -935,6 +936,14 @@ bool SESSION::CSession::SeekTime(double seekTime, bool& isError) // NOTE: It is assumed that the streams are ordered by video type (on m_streams), so we will seek first the video stream // to get the closest sample PTS to the requested seek time, then we will use to seek/align the other streams + + // PTS of the video sample actually delivered (reader/native domain). Once known, audio + // streams are aligned directly to it instead of via the manifest timing: Kodi synchronises + // on the emitted PTS, and audio/video share the source PTS clock, so this avoids the drift + // between the audio and video manifest timelines that would otherwise leave audio starting + // seconds after the picture on a seek. + std::optional videoReaderPts; + for (auto& stream : m_streams) { ISampleReader* streamReader{stream->GetReader()}; @@ -967,7 +976,38 @@ bool SESSION::CSession::SeekTime(double seekTime, bool& isError) } } - if (!SeekReader(*stream, seekTimePts)) + // Align the audio to the video sample actually delivered (co-timed in the reader PTS + // domain Kodi uses for A/V sync), rather than seeking it independently through its own + // manifest timing which drifts from the video timeline deep into the recording. This also + // covers resume-from-position, where Kodi issues the seek before the readers are started. + // hasAdStream excludes fMP4 audio muxed into the video stream (it has no own segment buffer + // and is seeked through the video reader); such a stream keeps its existing behaviour. + const ISampleReader::Type readerType{streamReader->GetType()}; + const bool alignToVideoPts = videoReaderPts.has_value() && hasAdStream && + stream->m_info.GetStreamType() == INPUTSTREAM_TYPE_AUDIO && + (readerType == ISampleReader::Type::TS || + readerType == ISampleReader::Type::ADTS || + readerType == ISampleReader::Type::FMP4); + + bool seekOk; + if (alignToVideoPts) + { + // SeekAdStream above reset the audio segment to its start, so a single forward scan + // lands straight on the video PTS whether or not the reader was already running. + const bool wasStarted = streamReader->IsStarted(); + seekOk = streamReader->TimeSeekReaderPts(*videoReaderPts); + + // Reproduce Start()'s first-start bookkeeping when the reader was not running yet + // (resume-from-position seeks before the first DemuxRead started the readers). + if (seekOk && !wasStarted && streamReader->GetInformation(stream->m_info)) + m_changed = true; + } + else + { + seekOk = SeekReader(*stream, seekTimePts); + } + + if (!seekOk) { streamReader->Reset(true); @@ -988,6 +1028,25 @@ bool SESSION::CSession::SeekTime(double seekTime, bool& isError) LOG::Log(LOGINFO, "Seek time %0.1lf for stream: %i continues at %0.1lf (PTS: %llu)", seekTime, streamReader->GetStreamId(), destTimeSecs, streamReader->PTS()); + // Report the residual A/V offset for streams aligned to the video PTS, so a bad landing + // (e.g. the audio segment did not contain the target and the reader snapped to a segment + // boundary) is immediately visible in the log instead of only as audible desync. + if (alignToVideoPts) + { + const int64_t avDeltaPts{static_cast(streamReader->PTS()) - + static_cast(*videoReaderPts)}; + // 0.5s: comfortably above one audio frame, below one segment - a larger delta means + // the co-timing did not land and playback will still be out of sync. + constexpr int64_t avDeltaWarnPts{STREAM_TIME_BASE / 2}; + const int64_t avDeltaAbs{avDeltaPts < 0 ? -avDeltaPts : avDeltaPts}; + + LOG::Log(avDeltaAbs > avDeltaWarnPts ? LOGWARNING : LOGINFO, + "Seek A/V align: audio stream %i landed at PTS %llu, target (video) PTS %llu, " + "delta %lld (%0.3lfs)", + streamReader->GetStreamId(), streamReader->PTS(), *videoReaderPts, avDeltaPts, + static_cast(avDeltaPts) / STREAM_TIME_BASE); + } + // We replace the seek time PTS initially requested with the PTS of the video sample found // in order to search the audio/subtitle sample packet more accurately. // f.e. in the case of MP4 the video packet has very few sample sync points @@ -997,6 +1056,8 @@ bool SESSION::CSession::SeekTime(double seekTime, bool& isError) if (stream->m_info.GetStreamType() == INPUTSTREAM_TYPE_VIDEO) { seekTime = destTimeSecs; + // Remember the native PTS delivered so the audio streams can be co-timed with it. + videoReaderPts = streamReader->PTS(); if (seekTimeCorrected != destTimePts) { diff --git a/src/samplereader/SampleReader.h b/src/samplereader/SampleReader.h index b6d80eff1..f0ffbab28 100644 --- a/src/samplereader/SampleReader.h +++ b/src/samplereader/SampleReader.h @@ -95,6 +95,24 @@ class ATTR_DLL_LOCAL ISampleReader */ virtual bool TimeSeek(uint64_t pts) = 0; + /*! + * \brief Seek the reader to an absolute reader PTS - the value domain returned by + * PTS() (native to this reader), not the manifest timing that TimeSeek() expects. + * Used after a seek to co-time a secondary stream (audio) with the video sample + * actually delivered: Kodi synchronises on the emitted PTS, and the audio/video + * share the source PTS clock, so aligning directly to the video's reader PTS avoids + * the drift between the audio and video manifest timelines that a manifest-domain + * seek (TimeSeek) reintroduces via GetPTSDiff(). + * \param pts The absolute reader PTS to align to. + * \return True if has success, otherwise false. + */ + virtual bool TimeSeekReaderPts(uint64_t pts) + { + // Default: remove the manifest compensation so TimeSeek lands on the given reader PTS. + const int64_t manifestPts{static_cast(pts) - GetPTSDiff()}; + return TimeSeek(manifestPts < 0 ? 0 : static_cast(manifestPts)); + } + virtual void SetPTSOffset(uint64_t offset) = 0; virtual int64_t GetPTSDiff() const = 0;