// Copyright Epic Games, Inc. All Rights Reserved. #include "ElectraProtronPlayerCache.h" FProtronVideoCache::FProtronVideoCache() { } void FProtronVideoCache::SetMaxFramesToCache(int32 InNumVideoFramesToCacheAhead, int32 InNumVideoFramesToCacheBehind) { NumVideoFramesToCacheAhead = InNumVideoFramesToCacheAhead; NumVideoFramesToCacheBehind = InNumVideoFramesToCacheBehind; MaxFramesToCache = NumVideoFramesToCacheAhead + NumVideoFramesToCacheBehind; Empty(); } void FProtronVideoCache::SetPlaybackRange(TRange InRange) { FScopeLock lock(&Lock); PlaybackRange = MoveTemp(InRange); PlaybackRangeEndInclusive = PlaybackRange.GetUpperBoundValue() - FTimespan(1); while(Entries.Num() && !Entries[0].RawRange.Overlaps(PlaybackRange)) { Entries.RemoveAt(0); } TRange TempInclusiveRange(PlaybackRange.GetLowerBoundValue(), PlaybackRange.GetUpperBoundValue() + FTimespan(1)); while(Entries.Num() && !Entries.Last().RawRange.Overlaps(TempInclusiveRange)) { Entries.Pop(); } } void FProtronVideoCache::SeekIssuedTo(FTimespan InNewPosition) { FScopeLock lock(&Lock); CurrentReadTime.Reset(); // Check if we have an entry for this time. int32 Idx = INDEX_NONE; for(int32 i=0,iMax=Entries.Num(); i= MaxFramesToCache) { return false; } int32 CurrentReadTimeIndex = Find(CurrentReadTime.GetValue()); check(CurrentReadTimeIndex != INDEX_NONE); // Playing forwards? TArray DummyIndices; int32 DummyWrapIndex; if (PlaybackRate >= 0.0f) { int32 HaveFuture = GetConsecutiveFutureSamples(DummyIndices, DummyWrapIndex, CurrentReadTimeIndex, true); return HaveFuture < NumVideoFramesToCacheAhead; } else { int32 HavePrevious = GetConsecutivePreviousSamples(DummyIndices, DummyWrapIndex, CurrentReadTimeIndex, true); return HavePrevious < NumVideoFramesToCacheAhead; } } void FProtronVideoCache::Empty() { FScopeLock lock(&Lock); Entries.Empty(); CurrentReadTime.Reset(); } FProtronVideoCache::EGetResult FProtronVideoCache::GetFrame(TSharedPtr& OutFrame, const TRange& InTimeRange, bool bIsLooping, bool bInReverse, bool bInUseFirstMatch) { auto WrappedModulo = [](FTimespan InTime, FTimespan InDuration) -> FTimespan { return (InTime >= FTimespan::Zero()) ? (InTime % InDuration) : (InDuration + (InTime % InDuration)); }; /* If the time range to find a frame for wraps around for looping then we need to inspect multiple sub ranges. Here we do not unwrap more than one loop index since this makes no difference. */ TRange AdjustedTimeRange(InTimeRange); if (AdjustedTimeRange.GetLowerBoundValue().GetTime() < PlaybackRange.GetLowerBoundValue()) { check(!"not handled"); } // Note: This should probably have happened in the player facade GetCurrentPlaybackTimeRange() already, but it doesn't. // All that happens there is checking if the range contains a wrap around, but not if the range as a whole needs to wrap! if (AdjustedTimeRange.GetLowerBoundValue().GetTime() > PlaybackRangeEndInclusive || AdjustedTimeRange.GetUpperBoundValue().GetTime() > PlaybackRangeEndInclusive) { check(!bInReverse); FTimespan s0 = WrappedModulo(AdjustedTimeRange.GetLowerBoundValue().GetTime(), PlaybackRange.GetUpperBoundValue()); FTimespan s1 = WrappedModulo(AdjustedTimeRange.GetUpperBoundValue().GetTime(), PlaybackRange.GetUpperBoundValue()); if (s0 < AdjustedTimeRange.GetLowerBoundValue().GetTime()) { AdjustedTimeRange.SetLowerBoundValue(FMediaTimeStamp(s0, AdjustedTimeRange.GetLowerBoundValue().GetSequenceIndex(), AdjustedTimeRange.GetLowerBoundValue().GetLoopIndex() + 1)); } if (s1 < AdjustedTimeRange.GetUpperBoundValue().GetTime()) { AdjustedTimeRange.SetUpperBoundValue(FMediaTimeStamp(s1, AdjustedTimeRange.GetUpperBoundValue().GetSequenceIndex(), AdjustedTimeRange.GetUpperBoundValue().GetLoopIndex() + 1)); } } TArray> RangesToCheck; TArray LoopIndices; // Does the time range loop? if (AdjustedTimeRange.GetLowerBoundValue().GetLoopIndex() == AdjustedTimeRange.GetUpperBoundValue().GetLoopIndex()) { // No. Add the range as it is to the list of ranges to check. RangesToCheck.Emplace(TRange(AdjustedTimeRange.GetLowerBoundValue().GetTime(), AdjustedTimeRange.GetUpperBoundValue().GetTime())); LoopIndices.Emplace(AdjustedTimeRange.GetLowerBoundValue().GetLoopIndex()); } // Looping forward? else if (AdjustedTimeRange.GetLowerBoundValue().GetLoopIndex() < AdjustedTimeRange.GetUpperBoundValue().GetLoopIndex()) { RangesToCheck.Emplace(TRange(AdjustedTimeRange.GetLowerBoundValue().GetTime(), PlaybackRange.GetUpperBoundValue())); RangesToCheck.Emplace(TRange(FTimespan::Zero(), AdjustedTimeRange.GetUpperBoundValue().GetTime())); LoopIndices.Emplace(AdjustedTimeRange.GetLowerBoundValue().GetLoopIndex()); LoopIndices.Emplace(AdjustedTimeRange.GetUpperBoundValue().GetLoopIndex()); } // Looping backward. else { RangesToCheck.Emplace(TRange(FTimespan::Zero(), AdjustedTimeRange.GetLowerBoundValue().GetTime())); RangesToCheck.Emplace(TRange(AdjustedTimeRange.GetUpperBoundValue().GetTime(), PlaybackRange.GetUpperBoundValue())); LoopIndices.Emplace(AdjustedTimeRange.GetLowerBoundValue().GetLoopIndex()); LoopIndices.Emplace(AdjustedTimeRange.GetUpperBoundValue().GetLoopIndex()); } FScopeLock lock(&Lock); FTimespan OverlapDuration(FTimespan::Zero()); int32 BestIndex = INDEX_NONE; TOptional LoopIndex; // Searching for forward play direction? if (!bInReverse) { // Do we want the first or last match? if (!bInUseFirstMatch) { // We want to find the *last* match with the largest overlap duration with the time range, // so we check the samples in reverse to find such a match fast. for(int32 nSubRangeIdx=RangesToCheck.Num()-1; nSubRangeIdx>=0; --nSubRangeIdx) { const TRange& TimeRange(RangesToCheck[nSubRangeIdx]); for(int32 SmpIdx=Entries.Num()-1; SmpIdx>=0; --SmpIdx) { if (TimeRange.Overlaps(Entries[SmpIdx].RawRange)) { FTimespan Overlap(TRange::Intersection(Entries[SmpIdx].RawRange, TimeRange).Size()); // No candidate or a better overlap? if (BestIndex == INDEX_NONE || Overlap > OverlapDuration) { OverlapDuration = Overlap; BestIndex = SmpIdx; LoopIndex = LoopIndices[nSubRangeIdx]; } else { break; } } } } } else { // We want to find the *first* match with the largest overlap duration. for(int32 nSubRangeIdx=0; nSubRangeIdx& TimeRange(RangesToCheck[nSubRangeIdx]); for(int32 SmpIdx=0; SmpIdx::Intersection(Entries[SmpIdx].RawRange, TimeRange).Size()); // No candidate or a better overlap? if (BestIndex == INDEX_NONE || Overlap > OverlapDuration) { OverlapDuration = Overlap; BestIndex = SmpIdx; LoopIndex = LoopIndices[nSubRangeIdx]; } else { break; } } } } } } // Searching for reverse play direction else { // Do we want the first or last match? if (!bInUseFirstMatch) { // We want to find the *last* match with the largest overlap duration with the time range // but in reverse direction, so we check the samples forwards to find such a match fast. for(int32 nSubRangeIdx=RangesToCheck.Num()-1; nSubRangeIdx>=0; --nSubRangeIdx) { const TRange& TimeRange(RangesToCheck[nSubRangeIdx]); for(int32 SmpIdx=0; SmpIdx::Intersection(Entries[SmpIdx].RawRange, TimeRange).Size()); // No candidate or a better overlap? if (BestIndex == INDEX_NONE || Overlap > OverlapDuration) { OverlapDuration = Overlap; BestIndex = SmpIdx; LoopIndex = LoopIndices[nSubRangeIdx]; } else { break; } } } } } else { // We want to find the *first* match with the largest overlap duration, // but in reverse direction. for(int32 nSubRangeIdx=0; nSubRangeIdx& TimeRange(RangesToCheck[nSubRangeIdx]); for(int32 SmpIdx=Entries.Num()-1; SmpIdx>=0; --SmpIdx) { if (TimeRange.Overlaps(Entries[SmpIdx].RawRange)) { FTimespan Overlap(TRange::Intersection(Entries[SmpIdx].RawRange, TimeRange).Size()); // No candidate or a better overlap? if (BestIndex == INDEX_NONE || Overlap > OverlapDuration) { OverlapDuration = Overlap; BestIndex = SmpIdx; LoopIndex = LoopIndices[nSubRangeIdx]; } else { break; } } } } } } // Found? if (BestIndex != INDEX_NONE) { OutFrame = Entries[BestIndex].Frame; // Set the sequence and loop index check(LoopIndex.IsSet()); static_cast(OutFrame.Get())->SetTime(FMediaTimeStamp(OutFrame->GetTime().GetTime(), AdjustedTimeRange.GetLowerBoundValue().GetSequenceIndex(), LoopIndex.GetValue())); CurrentReadTime = Entries[BestIndex].RawRange.GetLowerBoundValue(); PerformMaintenance(BestIndex, bIsLooping, bInReverse); return EGetResult::Hit; } // Not found. else { // If there are samples we need to dump all of them and return that fact. if (Entries.Num()) { Empty(); return EGetResult::PurgedEmpty; } return EGetResult::Miss; } } void FProtronVideoCache::PerformMaintenance(int32 InAtIndex, bool bIsLooping, bool bInReverse) { TArray NextIndices, PrevIndices; int32 NextIndexWrap, PrevIndexWrap; GetConsecutiveFutureSamples(NextIndices, NextIndexWrap, InAtIndex, bIsLooping); GetConsecutivePreviousSamples(PrevIndices, PrevIndexWrap, InAtIndex, bIsLooping); // If the next indices contains the oldest then we have the entire video in the cache. // So either the video is really short or the cache massively large. // Either way, we do not need to perform any evicting of frames. if (NextIndices.Num() && PrevIndices.Num() && NextIndices.Contains(PrevIndices.Last())) { return; } // Perform cache maintenance. if (!bInReverse) { if (PrevIndices.Num() > NumVideoFramesToCacheBehind) { // Remove the indices of the samples we want to keep. PrevIndices.RemoveAt(0, NumVideoFramesToCacheBehind); // Check the indices we want to remove for being a non-wrapping range. if (PrevIndices[0] >= PrevIndices.Last()) { Entries.RemoveAt(PrevIndices.Last(), PrevIndices[0] - PrevIndices.Last() + 1); } else { check(PrevIndexWrap != INDEX_NONE); PrevIndexWrap -= NumVideoFramesToCacheBehind; if (PrevIndexWrap < 0) { PrevIndexWrap = 0; } // First remove the range with the larger indices, which ensures the array doesn't shift down. Entries.RemoveAt(PrevIndices.Last(), PrevIndices[PrevIndexWrap] - PrevIndices.Last() + 1); // Then, if there is still a valid wrap index, remove the ones with the smaller indices which causes the // array to shift down. if (PrevIndexWrap > 0) { Entries.RemoveAt(PrevIndices[PrevIndexWrap - 1], PrevIndices[0] - PrevIndices[PrevIndexWrap - 1] + 1); } } } } else { if (NextIndices.Num() > NumVideoFramesToCacheBehind) { // Remove the indices of the samples we want to keep. NextIndices.RemoveAt(0, NumVideoFramesToCacheBehind); // Check the indices we want to remove for being a non-wrapping range. if (NextIndices[0] <= NextIndices.Last()) { Entries.RemoveAt(NextIndices[0], NextIndices.Last() - NextIndices[0] + 1); } else { check(NextIndexWrap != INDEX_NONE); NextIndexWrap -= NumVideoFramesToCacheBehind; if (NextIndexWrap < 0) { NextIndexWrap = 0; } // First remove the range with the larger indices, like above. Here these are at the start of the list. if (NextIndexWrap > 0) { Entries.RemoveAt(NextIndices[0], NextIndices[NextIndexWrap - 1] - NextIndices[0] + 1); } Entries.RemoveAt(NextIndices[NextIndexWrap], NextIndices.Last() - NextIndices[NextIndexWrap] + 1); } } } } int32 FProtronVideoCache::GetConsecutiveFutureSamples(TArray& OutIndices, int32& OutWrapIndex, int32 InStartFrameIndex, bool bInLooping) { TRange SampleRange(Entries[InStartFrameIndex].RawRange); int32 curIdx = InStartFrameIndex; OutWrapIndex = INDEX_NONE; for(int32 i=1,iMax=Entries.Num(); i NextSampleRange(Entries[nextIdx].RawRange); // Wrap around? if (nextIdx < curIdx) { // When not considering looping we are done. if (!bInLooping) { break; } // Are we wrapping around on the last sample of the range? // If this is not the last sample then we are not looping back to the beginning. if (!SampleRange.Contains(PlaybackRangeEndInclusive)) { break; } // If the next sample range, which is the range of the lowest sample in the cache, // does not contain the start of the play range then this is not a contiguous loop. if (!NextSampleRange.Contains(PlaybackRange.GetLowerBoundValue())) { break; } else { OutWrapIndex = OutIndices.Num(); OutIndices.Emplace(nextIdx); } } else { if (NextSampleRange.Adjoins(SampleRange)) { OutIndices.Emplace(nextIdx); } else { break; } } curIdx = nextIdx; SampleRange = MoveTemp(NextSampleRange); } return OutIndices.Num(); } int32 FProtronVideoCache::GetConsecutivePreviousSamples(TArray& OutIndices, int32& OutWrapIndex, int32 InStartFrameIndex, bool bInLooping) { TRange SampleRange(Entries[InStartFrameIndex].RawRange); int32 curIdx = InStartFrameIndex; OutWrapIndex = INDEX_NONE; for(int32 i=1,iMax=Entries.Num(); i PrevSampleRange(Entries[nextIdx].RawRange); // Wrap around? if (nextIdx > curIdx) { // When not considering looping we are done. if (!bInLooping) { break; } // Are we wrapping around on the first sample of the range? // If this is not the first sample then we are not looping back to the end. if (!SampleRange.Contains(PlaybackRange.GetLowerBoundValue())) { break; } // If the previous sample range, which is the range of the highest sample in the cache, // does not contain the end of the play range then this is not a contiguous loop. if (!PrevSampleRange.Contains(PlaybackRangeEndInclusive)) { break; } else { OutWrapIndex = OutIndices.Num(); OutIndices.Emplace(nextIdx); } } else { if (PrevSampleRange.Adjoins(SampleRange)) { OutIndices.Emplace(nextIdx); } else { break; } } curIdx = nextIdx; SampleRange = MoveTemp(PrevSampleRange); } return OutIndices.Num(); } void FProtronVideoCache::AddFrame(TSharedRef InFrame, FTimespan InRawPTS, FTimespan InRawDuration) { /* Note: We must not try to make room in the cache here. If we do that then we basically allow the decoder to run freely from start to end as we would be throwing out frames that haven't been used yet to make room for the next frame to add. */ FScopeLock lock(&Lock); // If there is no known time of the sample value last returned in GetFrame(), which is the case at the // start or after a seek, then we set that time to the time of the sample being delivered now. // That will become the time around which we check which cache entries to keep and which to evict. if (!CurrentReadTime.IsSet()) { CurrentReadTime = InRawPTS; } int32 Idx = Find(InRawPTS); if (Idx == INDEX_NONE) { FEntry ne; ne.RawRange = TRange(InRawPTS, InRawPTS + InRawDuration); ne.Frame = MoveTemp(InFrame); Entries.Emplace(MoveTemp(ne)); Entries.Sort([](const FEntry& a, const FEntry& b) { return a.RawRange.GetLowerBoundValue() < b.RawRange.GetLowerBoundValue(); }); } } void FProtronVideoCache::QueryCacheState(TRangeSet& OutTimeRanges) { FScopeLock lock(&Lock); for(auto &e : Entries) { OutTimeRanges.Add(e.RawRange); } }