// Copyright Epic Games, Inc. All Rights Reserved. #include "ElectraProtronPlayerImpl.h" #include "ElectraProtronPrivate.h" #include "IElectraCodecFactoryModule.h" #include "IElectraCodecFactory.h" #include "IElectraDecoderFeaturesAndOptions.h" #include "Utils/MPEG/ElectraUtilsMPEGVideo_H264.h" #include "Utils/MPEG/ElectraUtilsMPEGVideo_H265.h" #include "Utils/MPEG/ElectraUtilsMPEGAudio.h" #include "TrackFormatInfo.h" void FElectraProtronPlayer::FImpl::FLoaderThread::StartThread(const FString& InFilename, const TSharedPtr& InSharedPlayParams) { if (!Thread) { bTerminateThread = false; OpenRequest.Filename = InFilename; OpenRequest.SharedPlayParams = InSharedPlayParams; WorkSignal.Signal(); Thread = FRunnableThread::Create(this, TEXT("Electra Protron Loader"), 0, TPri_Normal); } } void FElectraProtronPlayer::FImpl::FLoaderThread::StopThread() { if (Thread) { bTerminateThread = true; Thread->WaitForCompletion(); } } uint32 FElectraProtronPlayer::FImpl::FLoaderThread::Run() { // TODO: clamp look ahead/behind values to 0 if negative and an internal limit if too large. while(!bTerminateThread) { WorkSignal.WaitTimeoutAndReset(1000 * 20); bool bNeedToLoad = false; FScopeLock lock(&LoadRequestLock); // Open a file? if (!OpenRequest.Filename.IsEmpty()) { SharedPlayParams = MoveTemp(OpenRequest.SharedPlayParams); check(SharedPlayParams.IsValid()); Reader = Electra::IFileDataReader::Create(); FString Filename = MoveTemp(OpenRequest.Filename); if (!Reader->Open(Filename)) { // Failure is not really an option seeing as how we already opened the file // successfully in Open(). check(!"how could this fail?"); LastErrorMessage = Reader->GetLastError(); Reader.Reset(); } } // New load request trigger by user interaction? else if (PendingLoadRequest.StartAtIterator.IsValid()) { // Only when there hasn't been an error yet. if (LastErrorMessage.IsEmpty()) { ActiveLoadRequest = MoveTemp(PendingLoadRequest); bNeedToLoad = true; } } // New update request? else if (ActiveLoadRequest.UpdateAtIterator.IsValid()) { check(!ActiveLoadRequest.StartAtIterator.IsValid()); ActiveLoadRequest.StartAtIterator = MoveTemp(ActiveLoadRequest.UpdateAtIterator); bNeedToLoad = true; } if (bNeedToLoad) { FMP4TrackSampleBufferPtr TrackSampleBuffer = ActiveLoadRequest.TrackSampleBuffer; FTrackIterator StartAtIterator = MoveTemp(ActiveLoadRequest.StartAtIterator); LoadRequestDirty = -1; check(StartAtIterator.IsValid()); lock.Unlock(); ELoadResult Result = Load(TrackSampleBuffer, StartAtIterator); if (Result == ELoadResult::Error) { if (LastErrorMessage.IsEmpty()) { LastErrorMessage = FString::Printf(TEXT("Error loading media samples")); } } } } PendingLoadRequest.Empty(); ActiveLoadRequest.Empty(); return 0; } void FElectraProtronPlayer::FImpl::FLoaderThread::CalcRangeToLoad(FSampleRange& OutRange, const FMP4TrackSampleBufferPtr& InTrackSampleBuffer, const FTrackIterator& InSampleIt) { OutRange.NumSamplesAfter = 0; OutRange.NumSamplesBefore = 0; OutRange.NumRemainingToLoadAfter = 0; OutRange.NumRemainingToLoadBefore = 0; // Given a track iterator, figure out the samples we need to load prior and following the iterator's current position. check(InSampleIt->IsValid()); check(InTrackSampleBuffer->FirstRangeSampleIt.IsValid() && InTrackSampleBuffer->LastRangeSampleIt.IsValid()); if (!InSampleIt->IsValid() || !InTrackSampleBuffer->FirstRangeSampleIt.IsValid() || !InTrackSampleBuffer->LastRangeSampleIt.IsValid()) { return; } const uint32 TrackTimescale = InSampleIt->GetTimescale(); check(TrackTimescale); int64 FwdDurNeeded = Electra::FTimeFraction(Config.DurationCacheAhead).GetAsTimebase(TrackTimescale); int64 RevDurNeeded = Electra::FTimeFraction(Config.DurationCacheBehind).GetAsTimebase(TrackTimescale); // See if the combined cache ahead and behind duration encompasses the entire playback range. if (FwdDurNeeded + RevDurNeeded >= InTrackSampleBuffer->LastRangeSampleIt->GetEffectiveDTS().GetNumerator() - InTrackSampleBuffer->FirstRangeSampleIt->GetEffectiveDTS().GetNumerator()) { const uint32 NumTrackSamples = InTrackSampleBuffer->LastRangeSampleIt->GetSampleNumber() + 1 - InTrackSampleBuffer->FirstRangeSampleIt->GetSampleNumber(); OutRange.NumSamplesAfter = SharedPlayParams->PlaybackDirection >= 0.0f ? NumTrackSamples * 3 / 4 : NumTrackSamples / 4; OutRange.NumSamplesBefore = NumTrackSamples - OutRange.NumSamplesAfter; OutRange.TimeRanges.Add(InTrackSampleBuffer->CurrentPlaybackRange); OutRange.SampleRanges.Add(TRange(InTrackSampleBuffer->FirstRangeSampleIt->GetSampleNumber(), InTrackSampleBuffer->LastRangeSampleIt->GetSampleNumber() + 1)); return; } if (SharedPlayParams->PlaybackDirection < 0.0f) { Swap(FwdDurNeeded, RevDurNeeded); } uint32 StartSampleNum = InSampleIt->GetSampleNumber(); int64 DurHandled = 0; FTrackIterator FwdTkIt(InSampleIt->Clone()); int64 StartDTS = FwdTkIt->GetEffectiveDTS().GetNumerator(); while(DurHandled < FwdDurNeeded) { const int64 dur = FwdTkIt->GetDuration().GetNumerator(); DurHandled += dur; ++OutRange.NumSamplesAfter; if (!FwdTkIt->NextEffective() || FwdTkIt->GetSampleNumber() >= InTrackSampleBuffer->LastRangeSampleIt->GetSampleNumber()) { OutRange.TimeRanges.Add(TRange(Electra::FTimeFraction(StartDTS, TrackTimescale).GetAsTimespan(), Electra::FTimeFraction(FwdTkIt->GetEffectiveDTS().GetNumerator() + dur, TrackTimescale).GetAsTimespan())); OutRange.SampleRanges.Add(TRange(StartSampleNum, FwdTkIt->GetSampleNumber()+1)); FwdTkIt = InTrackSampleBuffer->FirstRangeSampleIt->Clone(); StartDTS = FwdTkIt->GetEffectiveDTS().GetNumerator(); StartSampleNum = FwdTkIt->GetSampleNumber(); } } OutRange.TimeRanges.Add(TRange(Electra::FTimeFraction(StartDTS, TrackTimescale).GetAsTimespan(), Electra::FTimeFraction(FwdTkIt->GetEffectiveDTS().GetNumerator(), TrackTimescale).GetAsTimespan())); OutRange.SampleRanges.Add(TRange(StartSampleNum, FwdTkIt->GetSampleNumber())); // Reverse scanning const bool bNeedKeyframe = !InTrackSampleBuffer->TrackAndCodecInfo->bIsKeyframeOnlyFormat; DurHandled = 0; FTrackIterator RevTkIt(InSampleIt->Clone()); int64 EndDTS = RevTkIt->GetEffectiveDTS().GetNumerator(); uint32 EndSampleNum = RevTkIt->GetSampleNumber(); bool bHaveEnough = DurHandled >= RevDurNeeded; while(!bHaveEnough) { if (RevTkIt->GetSampleNumber() <= InTrackSampleBuffer->FirstRangeSampleIt->GetSampleNumber() || !RevTkIt->PrevEffective()) { TRange SmpRng(RevTkIt->GetSampleNumber(), EndSampleNum); if (!SmpRng.IsEmpty()) { int64 DTS = RevTkIt->GetEffectiveDTS().GetNumerator(); FTimespan ts = Electra::FTimeFraction(DTS, TrackTimescale).GetAsTimespan(); FTimespan te = Electra::FTimeFraction(EndDTS, TrackTimescale).GetAsTimespan(); OutRange.TimeRanges.Add(TRange(ts, te)); OutRange.SampleRanges.Add(SmpRng); } RevTkIt = InTrackSampleBuffer->LastRangeSampleIt->Clone(); EndDTS = RevTkIt->GetEffectiveDTS().GetNumerator() + RevTkIt->GetDuration().GetNumerator(); EndSampleNum = RevTkIt->GetSampleNumber() + 1; } const int64 dur = RevTkIt->GetDuration().GetNumerator(); DurHandled += dur; ++OutRange.NumSamplesBefore; bHaveEnough = DurHandled >= RevDurNeeded; if (bHaveEnough && bNeedKeyframe) { bHaveEnough = RevTkIt->IsSyncOrRAPSample(); } } TRange SmpRng(RevTkIt->GetSampleNumber(), EndSampleNum); if (!SmpRng.IsEmpty()) { int64 DTS = RevTkIt->GetEffectiveDTS().GetNumerator(); FTimespan ts = Electra::FTimeFraction(DTS, TrackTimescale).GetAsTimespan(); FTimespan te = Electra::FTimeFraction(EndDTS, TrackTimescale).GetAsTimespan(); OutRange.TimeRanges.Add(TRange(ts, te)); OutRange.SampleRanges.Add(TRange(RevTkIt->GetSampleNumber(), EndSampleNum)); } } void FElectraProtronPlayer::FImpl::FLoaderThread::GetUnreferencedFrames(TArray& OutFramesToRemove, const FMP4TrackSampleBufferPtr& InTrackSampleBuffer, const FSampleRange& InActiveSampleRange) { TArray FramesInMap; InTrackSampleBuffer->Lock.Lock(); FramesInMap.Reserve(InTrackSampleBuffer->SampleMap.Num()); InTrackSampleBuffer->SampleMap.GenerateKeyArray(FramesInMap); InTrackSampleBuffer->Lock.Unlock(); for(int32 i=0, iMax=FramesInMap.Num(); iLock.Lock(); uint32 SampleNum = InAtIterator->GetSampleNumber(); FMP4SamplePtr Sample = InFromBuffer->SampleMap.FindRef(SampleNum); InFromBuffer->Lock.Unlock(); // If the buffer is still the one that is active we need to trigger a fetch of new samples // regardless of whether we got (and thus consumed) the sample asked for. // We should have the sample ready. If not then triggering the load request is especially important. LoadRequestLock.Lock(); if (ActiveLoadRequest.TrackSampleBuffer == InFromBuffer) { ActiveLoadRequest.UpdateAtIterator = InAtIterator->Clone(); LoadRequestDirty = (int32) SampleNum; WorkSignal.Signal(); } LoadRequestLock.Unlock(); return Sample; } TRangeSet FElectraProtronPlayer::FImpl::FLoaderThread::GetTimeRangesToLoad() { FScopeLock lock(&TimeRangeLock); return TimeRangesToLoad; } void FElectraProtronPlayer::FImpl::FLoaderThread::SetPlaybackRange(TRange InRange) { TimeRangeLock.Lock(); PlaybackRange = MoveTemp(InRange); TimeRangeLock.Unlock(); } void FElectraProtronPlayer::FImpl::FLoaderThread::RequestLoad(FMP4TrackSampleBufferPtr InTrackSampleBuffer, FTimespan InTime) { if (!Reader.IsValid() || !InTrackSampleBuffer.IsValid()) { return; } // Did the playback range change? TimeRangeLock.Lock(); TRange RangeNow(PlaybackRange); TimeRangeLock.Unlock(); if (RangeNow != InTrackSampleBuffer->CurrentPlaybackRange) { InTrackSampleBuffer->CurrentPlaybackRange = RangeNow; // Locate the first and last sample numbers for the range FTrackIterator RangeIt = InTrackSampleBuffer->TrackAndCodecInfo->MP4Track->CreateIteratorAtKeyframe(Electra::FTimeValue().SetFromTimespan(RangeNow.GetLowerBoundValue()), Electra::FTimeValue::GetZero()); if (!RangeIt.IsValid()) { LastErrorMessage = InTrackSampleBuffer->TrackAndCodecInfo->MP4Track->GetLastError(); UE_LOG(LogElectraProtron, Error, TEXT("%s"), *LastErrorMessage); return; } InTrackSampleBuffer->FirstRangeSampleIt = RangeIt->Clone(); // Move forward until we reach the end or both the effective DTS *and* PTS are greater or equal than the end of the range. // We need to look at both DTS and PTS because the effective PTS can be smaller than the effective DTS due to composition time offsets. while(!RangeIt->IsLastEffective()) { if (RangeIt->GetEffectiveDTS().GetAsTimespan() >= RangeNow.GetUpperBoundValue() && RangeIt->GetEffectivePTS().GetAsTimespan() >= RangeNow.GetUpperBoundValue()) { // We want the last iterator to represent the last sample included in the playback range, // so we need to step one back here as we are currently outside the range. RangeIt->PrevEffective(); break; } RangeIt->NextEffective(); } InTrackSampleBuffer->LastRangeSampleIt = MoveTemp(RangeIt); } FTrackIterator TkIt = InTrackSampleBuffer->TrackAndCodecInfo->MP4Track->CreateIteratorAtKeyframe(Electra::FTimeValue().SetFromTimespan(InTime), Electra::FTimeValue().SetFromMilliseconds(InTrackSampleBuffer->TrackAndCodecInfo->bIsKeyframeOnlyFormat ? 0 : Config.NextKeyframeThresholdMillis)); if (!TkIt.IsValid()) { LastErrorMessage = InTrackSampleBuffer->TrackAndCodecInfo->MP4Track->GetLastError(); UE_LOG(LogElectraProtron, Error, TEXT("%s"), *LastErrorMessage); return; } LoadRequestLock.Lock(); PendingLoadRequest.Empty(); PendingLoadRequest.TrackSampleBuffer = MoveTemp(InTrackSampleBuffer); PendingLoadRequest.StartAtIterator = MoveTemp(TkIt); LoadRequestDirty = -2; WorkSignal.Signal(); LoadRequestLock.Unlock(); } FElectraProtronPlayer::FImpl::FLoaderThread::ELoadResult FElectraProtronPlayer::FImpl::FLoaderThread::Load(FMP4TrackSampleBufferPtr InTrackSampleBuffer, FTrackIterator InAtIterator) { // Determine the range of samples to load. check(InAtIterator.IsValid()); check(InTrackSampleBuffer.IsValid()); FSampleRange RangeToLoad; CalcRangeToLoad(RangeToLoad, InTrackSampleBuffer, InAtIterator); // Need to load something... check(RangeToLoad.NumSamplesAfter || RangeToLoad.NumSamplesBefore); if (!(RangeToLoad.NumSamplesAfter || RangeToLoad.NumSamplesBefore)) { return ELoadResult::Ok; } TimeRangeLock.Lock(); TimeRangesToLoad = RangeToLoad.TimeRanges; TimeRangeLock.Unlock(); // Get the frames that are not referenced now we have to evict from the data map. TArray FramesToRemove; GetUnreferencedFrames(FramesToRemove, InTrackSampleBuffer, RangeToLoad); // Are timecodes from another track referenced? FTrackIterator FwdTkItTC, RevTkItTC; ElectraProtronUtils::FCodecInfo::FTMCDTimecode TimecodeInfo; if (Config.bReadSampleTimecode && LoaderTypeIndex == CodecTypeIndex(ElectraProtronUtils::FCodecInfo::EType::Video)) { if (auto TimecodeTrack = InTrackSampleBuffer->TrackAndCodecInfo->ReferencedTimecodeTrack.Pin()) { // The timecode track needs to have as many samples as this track, otherwise there would be // a mismatch somewhere and the timecode couldn't be used. if (TimecodeTrack->MP4Track->GetNumberOfSamples() == InTrackSampleBuffer->TrackAndCodecInfo->MP4Track->GetNumberOfSamples()) { // Get the timecode description from the codec info. TimecodeInfo = TimecodeTrack->CodecInfo.Properties.Get(); FwdTkItTC = TimecodeTrack->MP4Track->CreateIterator(InAtIterator->GetSampleNumber()); if (FwdTkItTC.IsValid()) { RevTkItTC = FwdTkItTC->Clone(); } } } } // Calculate the ratio of samples to fetch ahead vs. fetch behind. // We want to fetch more samples in the direction we're going than from // where we came. // For this we do not need to look at the playback direction, we just take // the number of samples to load in either direction since that is determined // by the current play direction. Whichever is the one with more samples is the // direction we are going in. double fwd = RangeToLoad.NumSamplesAfter; double rev = RangeToLoad.NumSamplesBefore; int32 RatioF = (fwd > rev && rev > 0.0) ? FMath::CeilToInt(fwd / rev) : 1; int32 RatioR = (rev > fwd && fwd > 0.0) ? FMath::CeilToInt(rev / fwd) : 1; FTrackIterator FwdTkIt(InAtIterator->Clone()); FTrackIterator RevTkIt(InAtIterator->Clone()); RangeToLoad.NumRemainingToLoadAfter = RangeToLoad.NumSamplesAfter; RangeToLoad.NumRemainingToLoadBefore = RangeToLoad.NumSamplesBefore; const int32 kMinFramesToLoad = 2; while(RangeToLoad.NumRemainingToLoadAfter || RangeToLoad.NumRemainingToLoadBefore) { // Remove one old frame now. if (FramesToRemove.Num()) { InTrackSampleBuffer->Lock.Lock(); InTrackSampleBuffer->SampleMap.Remove(FramesToRemove.Last()); InTrackSampleBuffer->Lock.Unlock(); FramesToRemove.Pop(EAllowShrinking::No); } for(int32 i=RatioF; i>0 && RangeToLoad.NumRemainingToLoadAfter; --i) { FMP4SamplePtr Sample = RetrieveSample(InTrackSampleBuffer, FwdTkIt, FwdTkItTC, TimecodeInfo); // Abort loading immediately or when we have loaded at least the minimum number of required frames? if (LoadRequestDirty < -1 || (LoadRequestDirty >= 0 && (-RangeToLoad.NumRemainingToLoadAfter + RangeToLoad.NumSamplesAfter > kMinFramesToLoad))) { //UE_LOG(LogTemp, Log, TEXT("Load canceled with %d/%d fwd and %d/%d rev remaining"), RangeToLoad.NumRemainingToLoadAfter, RangeToLoad.NumSamplesAfter, RangeToLoad.NumRemainingToLoadBefore, RangeToLoad.NumSamplesBefore); return ELoadResult::Canceled; } if (!Sample.IsValid()) { return ELoadResult::Error; } if (FwdTkItTC.IsValid()) { FwdTkItTC->Next(); } if (FwdTkIt->GetSampleNumber() >= InTrackSampleBuffer->LastRangeSampleIt->GetSampleNumber() || !FwdTkIt->NextEffective()) { FwdTkIt = InTrackSampleBuffer->FirstRangeSampleIt->Clone(); FwdTkItTC.Reset(); if (auto TimecodeTrack = InTrackSampleBuffer->TrackAndCodecInfo->ReferencedTimecodeTrack.Pin()) { FwdTkItTC = TimecodeTrack->MP4Track->CreateIterator(FwdTkIt->GetSampleNumber()); } } --RangeToLoad.NumRemainingToLoadAfter; } for(int32 i=RatioR; i>0 && RangeToLoad.NumRemainingToLoadBefore; --i) { if (RevTkItTC.IsValid()) { RevTkItTC->Prev(); } if (RevTkIt->GetSampleNumber() <= InTrackSampleBuffer->FirstRangeSampleIt->GetSampleNumber() || !RevTkIt->PrevEffective()) { RevTkIt = InTrackSampleBuffer->LastRangeSampleIt->Clone(); RevTkItTC.Reset(); if (auto TimecodeTrack = InTrackSampleBuffer->TrackAndCodecInfo->ReferencedTimecodeTrack.Pin()) { RevTkItTC = TimecodeTrack->MP4Track->CreateIterator(RevTkIt->GetSampleNumber()); } } FMP4SamplePtr Sample = RetrieveSample(InTrackSampleBuffer, RevTkIt, RevTkItTC, TimecodeInfo); // Abort loading immediately or when we have loaded at least the minimum number of required frames? if (LoadRequestDirty < -1 || (LoadRequestDirty >= 0 && (-RangeToLoad.NumRemainingToLoadBefore + RangeToLoad.NumSamplesBefore > kMinFramesToLoad))) { //UE_LOG(LogTemp, Log, TEXT("Load canceled with %d/%d fwd and %d/%d rev remaining"), RangeToLoad.NumRemainingToLoadAfter, RangeToLoad.NumSamplesAfter, RangeToLoad.NumRemainingToLoadBefore, RangeToLoad.NumSamplesBefore); return ELoadResult::Canceled; } if (!Sample.IsValid()) { return ELoadResult::Error; } --RangeToLoad.NumRemainingToLoadBefore; } } // Remove any remaining old frames now. InTrackSampleBuffer->Lock.Lock(); for(int32 i=0,iMax=FramesToRemove.Num(); iSampleMap.Remove(FramesToRemove[i]); } InTrackSampleBuffer->Lock.Unlock(); return ELoadResult::Ok; } TSharedPtr FElectraProtronPlayer::FImpl::FLoaderThread::RetrieveSample(const FMP4TrackSampleBufferPtr& InTrackSampleBuffer, const FTrackIterator& InSampleIt, const FTrackIterator& InOptionalTimecodeIt, const ElectraProtronUtils::FCodecInfo::FTMCDTimecode& InTimecodeInfo) { InTrackSampleBuffer->Lock.Lock(); FMP4SamplePtr* CurrentSample = InTrackSampleBuffer->SampleMap.Find(InSampleIt->GetSampleNumber()); InTrackSampleBuffer->Lock.Unlock(); if (CurrentSample) { check((*CurrentSample)->DTS == InSampleIt->GetDTS().GetAsTimespan()); check((*CurrentSample)->PTS == InSampleIt->GetPTS().GetAsTimespan()); check((*CurrentSample)->EffectiveDTS == InSampleIt->GetEffectiveDTS().GetAsTimespan()); check((*CurrentSample)->EffectivePTS == InSampleIt->GetEffectivePTS().GetAsTimespan()); check((*CurrentSample)->Duration == InSampleIt->GetDurationAsTimespan()); check((*CurrentSample)->SizeInBytes == InSampleIt->GetSampleSize()); check((*CurrentSample)->OffsetInFile == InSampleIt->GetSampleFileOffset()); check((*CurrentSample)->TrackID == InSampleIt->GetTrackID()); check((*CurrentSample)->SampleNumber == InSampleIt->GetSampleNumber()); check((*CurrentSample)->bIsSyncOrRap == InSampleIt->IsSyncOrRAPSample()); return *CurrentSample; } auto CheckAbort = Electra::IBaseDataReader::FCancellationCheckDelegate::CreateLambda([&]() { // We do NOT abort loading of a frame! return false; }); FMP4SamplePtr Sample = MakeShared(); Sample->DTS = InSampleIt->GetDTS().GetAsTimespan(); Sample->PTS = InSampleIt->GetPTS().GetAsTimespan(); Sample->EffectiveDTS = InSampleIt->GetEffectiveDTS().GetAsTimespan(); Sample->EffectivePTS = InSampleIt->GetEffectivePTS().GetAsTimespan(); Sample->Duration = InSampleIt->GetDurationAsTimespan(); Sample->SizeInBytes = InSampleIt->GetSampleSize(); Sample->OffsetInFile = InSampleIt->GetSampleFileOffset(); Sample->TrackID = InSampleIt->GetTrackID(); Sample->SampleNumber = InSampleIt->GetSampleNumber(); Sample->bIsSyncOrRap = InSampleIt->IsSyncOrRAPSample(); Sample->Data.SetNumUninitialized(Sample->SizeInBytes); int64 NumRead = Reader->ReadData(Sample->Data.GetData(), Sample->SizeInBytes, Sample->OffsetInFile, CheckAbort); if (NumRead != Sample->SizeInBytes) { return nullptr; } // Optionally read the timecode sample from the associated track if (InOptionalTimecodeIt.IsValid()) { int64 tcSampleSize = InOptionalTimecodeIt->GetSampleSize(); if (tcSampleSize == 4) { TArray TimecodeBuffer; TimecodeBuffer.SetNumUninitialized(Align(tcSampleSize, 4)); NumRead = Reader->ReadData(TimecodeBuffer.GetData(), tcSampleSize, InOptionalTimecodeIt->GetSampleFileOffset(), CheckAbort); if (NumRead == tcSampleSize) { Sample->AssociatedTimecode = InTimecodeInfo.ConvertToTimecode(Electra::GetFromBigEndian(TimecodeBuffer[0])); Sample->AssociatedTimecodeFramerate = InTimecodeInfo.GetFrameRate(); } } } uint32 SampleNumber = Sample->SampleNumber; FScopeLock lock(&InTrackSampleBuffer->Lock); InTrackSampleBuffer->SampleRanges.Add(TRange(SampleNumber, SampleNumber+1)); InTrackSampleBuffer->SampleMap.Emplace(SampleNumber, Sample); return Sample; }