using System; using System.Collections.Generic; namespace hdsdump.f4f { public class AdobeFragmentRunTable : FullBox { public uint timeScale; public List<string> qualitySegmentURLModifiers = new List<string>(); public List<FragmentDurationPair> fragmentDurationPairs = new List<FragmentDurationPair>(); public override void Parse(BoxInfo bi, HDSBinaryReader br) { base.Parse(bi, br); timeScale = br.ReadUInt32(); qualitySegmentURLModifiers.Clear(); uint qualityEntryCount = br.ReadByte(); for (uint i = 0; i < qualityEntryCount; i++) { qualitySegmentURLModifiers.Add(br.ReadString()); } uint entryCount = br.ReadUInt32(); for (uint i = 0; i < entryCount; i++) { FragmentDurationPair fdp = new FragmentDurationPair(); fdp.Parse(br); fragmentDurationPairs.Add(fdp); } } /// <summary> /// Given a time spot in terms of the time scale used by the fragment table, returns the corresponding /// Id of the fragment that contains the time spot. /// </summary> public FragmentAccessInformation findFragmentIdByTime(uint time, uint totalDuration, bool live = false) { if (fragmentDurationPairs.Count <= 0) return null; FragmentDurationPair fdp = null; for (int i = 1; i < fragmentDurationPairs.Count; i++) { fdp = fragmentDurationPairs[i]; if (fdp.durationAccrued >= time) { return validateFragment(calculateFragmentId(fragmentDurationPairs[i - 1], time), totalDuration, live); } } return validateFragment(calculateFragmentId(fragmentDurationPairs[fragmentDurationPairs.Count - 1], time), totalDuration, live); } /// <summary> /// Given a fragment id, check whether the current fragment is valid or a discontinuity. /// If the latter, skip to the nearest fragment and return the new fragment id. /// /// return the Id of the fragment that is valid. /// </summary> public FragmentAccessInformation validateFragment(uint fragId, ulong totalDuration, bool live = false) { int size = fragmentDurationPairs.Count - 1; FragmentAccessInformation fai = null; uint timeResidue, timeDistance, fragStartTime; for (int i = 0; i < size; i++) { FragmentDurationPair curFdp = fragmentDurationPairs[i]; FragmentDurationPair nextFdp = fragmentDurationPairs[i + 1]; if ((curFdp.firstFragment <= fragId) && (fragId < nextFdp.firstFragment)) { if (curFdp.duration <= 0) { fai = getNextValidFragment(i + 1); } else { fai = new FragmentAccessInformation(); fai.fragId = fragId; fai.fragDuration = curFdp.duration; fai.fragmentEndTime = (uint)curFdp.durationAccrued + curFdp.duration * (fragId - curFdp.firstFragment + 1); } break; } else if ((curFdp.firstFragment <= fragId) && endOfStreamEntry(nextFdp)) { if (curFdp.duration > 0) { timeResidue = (uint)(totalDuration - curFdp.durationAccrued); timeDistance = (fragId - curFdp.firstFragment + 1) * curFdp.duration; fragStartTime = (fragId - curFdp.firstFragment) * curFdp.duration; if (timeResidue > fragStartTime) { if (!live || ((fragStartTime + curFdp.duration + curFdp.durationAccrued) <= totalDuration)) { fai = new FragmentAccessInformation(); fai.fragId = fragId; fai.fragDuration = curFdp.duration; if (timeResidue >= timeDistance) { fai.fragmentEndTime = (uint)curFdp.durationAccrued + timeDistance; } else { fai.fragmentEndTime = (uint)curFdp.durationAccrued + timeResidue; } break; } } } } } if (fai == null) { FragmentDurationPair lastFdp = fragmentDurationPairs[size]; if (lastFdp.duration > 0 && fragId >= lastFdp.firstFragment) { timeResidue = (uint)(totalDuration - lastFdp.durationAccrued); timeDistance = (fragId - lastFdp.firstFragment + 1) * lastFdp.duration; fragStartTime = (fragId - lastFdp.firstFragment) * lastFdp.duration; if (timeResidue > fragStartTime) { if (!live || ((fragStartTime + lastFdp.duration + lastFdp.durationAccrued) <= totalDuration)) { fai = new FragmentAccessInformation(); fai.fragId = fragId; fai.fragDuration = lastFdp.duration; if (timeResidue >= timeDistance) { fai.fragmentEndTime = (uint)lastFdp.durationAccrued + timeDistance; } else { fai.fragmentEndTime = (uint)lastFdp.durationAccrued + timeResidue; } } } } } return fai; } private FragmentAccessInformation getNextValidFragment(int startIdx) { FragmentAccessInformation fai = null; for (int i = startIdx; i < fragmentDurationPairs.Count; i++) { FragmentDurationPair fdp = fragmentDurationPairs[i]; if (fdp.duration > 0) { fai = new FragmentAccessInformation(); fai.fragId = fdp.firstFragment; fai.fragDuration = fdp.duration; fai.fragmentEndTime = (uint)fdp.durationAccrued + fdp.duration; break; } } return fai; } private bool endOfStreamEntry(FragmentDurationPair fdp) { return (fdp.duration == 0 && fdp.discontinuityIndicator == 0); } /// <summary> /// Given a fragment id, return the number of fragments after the /// fragment with the id given. /// </summary> public uint fragmentsLeft(uint fragId, uint currentMediaTime) { if (fragmentDurationPairs == null || fragmentDurationPairs.Count == 0) { return 0; } FragmentDurationPair fdp = fragmentDurationPairs[fragmentDurationPairs.Count - 1] as FragmentDurationPair; uint fragments = (currentMediaTime - (uint)fdp.durationAccrued) / fdp.duration + fdp.firstFragment - fragId - 1; return fragments; } /// <summary> /// return whether the fragment table is complete. /// </summary> public bool tableComplete() { if (fragmentDurationPairs == null || fragmentDurationPairs.Count <= 0) { return false; } FragmentDurationPair fdp = fragmentDurationPairs[fragmentDurationPairs.Count - 1] as FragmentDurationPair; return (fdp.duration == 0 && fdp.discontinuityIndicator == 0); } public void adjustEndEntryDurationAccrued(uint value) { FragmentDurationPair fdp = fragmentDurationPairs[fragmentDurationPairs.Count - 1]; if (fdp.duration == 0) { fdp.durationAccrued = value; } } public uint getFragmentDuration(uint fragId) { int i = 0; while ((i < fragmentDurationPairs.Count) && (fragmentDurationPairs[i].firstFragment <= fragId)) { i++; } if (i > 0) return fragmentDurationPairs[i - 1].duration; else return 0; } /// <summary> /// return the first FragmentDurationPair whose index >= i that is not a discontinuity, /// or null if no such FragmentDurationPair exists. /// </summary> private FragmentDurationPair findNextValidFragmentDurationPair(int index) { for (int i = index; i < fragmentDurationPairs.Count; ++i) { FragmentDurationPair fdp = fragmentDurationPairs[i]; if (fdp.duration > 0) { return fdp; } } return null; } /// <summary> /// return the first FragmentDurationPair whose index less i that is not a discontinuity, /// or null if no such FragmentDurationPair exists. /// </summary> private FragmentDurationPair findPrevValidFragmentDurationPair(int index) { int i = index; if (i > fragmentDurationPairs.Count) { i = fragmentDurationPairs.Count; } for (; i > 0; --i) { FragmentDurationPair fdp = fragmentDurationPairs[i - 1]; if (fdp.duration > 0) { return fdp; } } return null; } private uint calculateFragmentId(FragmentDurationPair fdp, uint time) { if (fdp.duration <= 0) { return fdp.firstFragment; } uint deltaTime = time - (uint)fdp.durationAccrued; uint count = (deltaTime > 0)? deltaTime / fdp.duration : 1; if ((deltaTime % fdp.duration) > 0) { count++; } return fdp.firstFragment + count - 1; } /// <summary> /// return the id of the first (non-discontinuity) fragment in the FRT, or 0 if no such fragment exists /// </summary> public uint firstFragmentId { get { FragmentDurationPair fdp = findNextValidFragmentDurationPair(0); if(fdp == null) { return 0; } return fdp.firstFragment; } } /// <summary> /// return true if the fragment is in a true gap within the middle of the content (discontinuity type 2). /// returns false if fragment less first fragment number /// returns false if fragment greater or equal last fragment number /// </summary> public bool isFragmentInGap(uint fragmentId) { bool inGap = false; forEachGap(delegate(FragmentDurationPair fdp, FragmentDurationPair prevFdp, FragmentDurationPair nextFdp) { uint gapStartFragmentId = fdp.firstFragment; uint gapEndFragmenId = nextFdp.firstFragment; if (gapStartFragmentId <= fragmentId && fragmentId<gapEndFragmenId) { inGap = true; } return !inGap; }); return inGap; } /// <summary> /// return true if the fragment is time is in a true gap within the middle of the content (discontinuity type 2). /// returns false if time is less time of the first fragment /// returns false if time is greater or equal time the last fragment /// </summary> public bool isTimeInGap(uint time, uint fragmentInterval) { bool inGap = false; forEachGap(delegate (FragmentDurationPair fdp, FragmentDurationPair prevFdp, FragmentDurationPair nextFdp) { uint prevEndTime = (uint)prevFdp.durationAccrued + prevFdp.duration* (fdp.firstFragment - prevFdp.firstFragment); uint nextStartTime = (uint)nextFdp.durationAccrued; uint idealGapStartTime = (Math.Max(fdp.firstFragment, 1)-1) * fragmentInterval; uint idealGapEndTime = (Math.Max(Math.Max(nextFdp.firstFragment, fdp.firstFragment + 1), 1) - 1) * fragmentInterval; uint gapStartTime = Math.Min(prevEndTime, idealGapStartTime); uint gapEndTime = Math.Max(nextStartTime, idealGapEndTime); if(gapStartTime <= time && time < gapEndTime) { inGap = true; } return !inGap; }); return inGap; } /// <summary> /// return the number of fragments within a gap (discontinuity 2) /// </summary> public uint countGapFragments() { uint count = 0; forEachGap(delegate (FragmentDurationPair fdp, FragmentDurationPair prevFdp, FragmentDurationPair nextFdp) { uint gapStartFragmentId = fdp.firstFragment; uint gapEndFragmentId = (uint)(Math.Max(nextFdp.firstFragment, gapStartFragmentId)); count += gapEndFragmentId - gapStartFragmentId; return true; }); return count; } /// <summary> /// calls f for each true gap (discontinuity of type 2) found within the FRT. f will be passed /// an Object argument (arg) with 3 fields. /// /// arg.fdp will be the discontinuity entry. /// arg.prevFdp will be the previous non-discontinuity entry /// arg.nextFdp will be the next non-discontinuity entry /// /// if f returns false, iteration will halt /// if f returns true, iteration will continue /// </summary> private void forEachGap(Func<FragmentDurationPair, FragmentDurationPair, FragmentDurationPair, bool> f) { if (fragmentDurationPairs.Count <= 0) { return; } // search for gaps, then check if the desired time is in that gap for(int i = 0; i < fragmentDurationPairs.Count; ++i) { FragmentDurationPair fdp = fragmentDurationPairs[i]; if (fdp.duration != 0 || fdp.discontinuityIndicator != 2) { // skip until we find a discontinuity of type 2 continue; } // gaps should only be present in the middle of content, // so there should always be a previous valid entry and // a next valid entry. // figure out the previous valid entry FragmentDurationPair prevFdp = findPrevValidFragmentDurationPair(i); if (prevFdp == null // very uncommon case: there are no non-discontinuities before the discontinuity || prevFdp.firstFragment > fdp.firstFragment) // very uncommon case: fragment numbers are out of order { continue; } // search forwards for the first non-discontinuity FragmentDurationPair nextFdp = findNextValidFragmentDurationPair(i+1); if(nextFdp == null // very uncommon case: there are no valid fragments after the discontinuity || fdp.firstFragment > nextFdp.firstFragment) // very uncommon case: fragment numbers are out of order { continue; } bool shouldContinue = f(fdp, prevFdp, nextFdp); if (!shouldContinue) return; } } /// <summary> /// return the fragment information for the first fragment in the FRT whose fragment number /// is greater than or equal to fragment id. special cases: /// /// if fragmentId is in a gap, the first fragment after the gap will be returned. /// if fragmentId is in a skip, the first fragment after the skip will be returned. /// if fragmentId is before the first fragment-duration-pair, the first fragment will be returned. /// if fragmentId is after the last fragment-duration-pair, it will be assumed to exist. /// (in other words, the live point is ignored). /// /// if there are no valid entries in the FRT, returns null. this is the only situation that returns null. /// </summary> public FragmentAccessInformation getFragmentWithIdGreq(uint fragmentId) { FragmentDurationPair desiredFdp = null; uint desiredFragmentId = 0; forEachInterval(delegate (FragmentDurationPair fdp, bool isLast, uint startFragmentId, uint endFragmentId, uint startTime, uint endTime) { if (fragmentId<startFragmentId) { // before the given interval desiredFdp = fdp; desiredFragmentId = startFragmentId; return false; // stop iterating } else if(isLast) { // catch all in the last entry desiredFdp = fdp; desiredFragmentId = fragmentId; return false; } else if(fragmentId<endFragmentId) { // between the start and end of this interval desiredFdp = fdp; desiredFragmentId = fragmentId; return false; // stop iterating } else { // beyond this interval, but not the last entry return true; // keep iterating } }); if(desiredFdp == null) { // no fragment entries case return null; } if(desiredFragmentId<desiredFdp.firstFragment) { // probably won't ever hit this // just make sure that we're before the start desiredFragmentId = desiredFdp.firstFragment; } FragmentAccessInformation fai = new FragmentAccessInformation(); fai.fragId = desiredFragmentId; fai.fragDuration = desiredFdp.duration; fai.fragmentEndTime = (uint)desiredFdp.durationAccrued + (desiredFragmentId - desiredFdp.firstFragment + 1) * desiredFdp.duration; return fai; } /// <summary> /// return the fragment information for the first fragment in the FRT that contains a time /// greater than or equal to fragment time. special cases: /// /// if time is in a gap, the first fragment after the gap will be returned. /// if time is in a skip, the first fragment after the skip will be returned. /// if time is before the first fragment-duration-pair, the first fragment will be returned. /// if time is after the last fragment-duration-pair, it will be assumed to exist. /// (in other words, the live point is ignored). /// /// if there are no valid entries in the FRT, returns null. this is the only situation that returns null. /// </summary> public FragmentAccessInformation getFragmentWithTimeGreq(uint fragmentTime) { FragmentDurationPair desiredFdp = null; uint desiredFragmentStartTime = 0; forEachInterval(delegate (FragmentDurationPair fdp, bool isLast, uint startFragmentId, uint endFragmentId, uint startTime, uint endTime) { if (fragmentTime < startTime) { // before the given interval desiredFdp = fdp; desiredFragmentStartTime = startTime; return false; // stop iterating } else if (isLast) { // catch all in the last entry desiredFdp = fdp; desiredFragmentStartTime = fragmentTime; return false; } else if(fragmentTime<endTime) { // between the start and end of this interval desiredFdp = fdp; desiredFragmentStartTime = fragmentTime; return false; // stop iterating } else { // beyond this interval, but not the last entry return true; // keep iterating } }); if (desiredFdp == null) { // no fragment entries case return null; } uint desiredFragmentId = calculateFragmentId(desiredFdp, desiredFragmentStartTime); FragmentAccessInformation fai = new FragmentAccessInformation(); fai.fragId = desiredFragmentId; fai.fragDuration = desiredFdp.duration; fai.fragmentEndTime = (uint)desiredFdp.durationAccrued + (desiredFragmentId - desiredFdp.firstFragment + 1) * desiredFdp.duration; return fai; } /// <summary> /// calls f for each set of fragments advertised by the FRT. f will be passed /// an Object argument (arg) with 6 fields. /// /// arg.fdp will be the entry corresponding to the fragment range. /// arg.isLast will be true if this is the last entry in the table /// arg.startFragmentId will be the id of the first fragment in the interval /// arg.endFragmentId will be the id of the last fragment in the interval + 1 /// arg.startTime will be the start time of the first fragment in the interval /// arg.endTime will be the end time of the last fragment in the interval /// /// if f returns false, iteration will halt /// if f returns true, iteration will continue /// /// if will be called in ascending startFragmentId order. /// </summary> private void forEachInterval(Func<FragmentDurationPair, bool, uint, uint, uint, uint, bool> f) { // search for gaps, then check if the desired time is in that gap for(int i = 0; i < fragmentDurationPairs.Count; ++i) { FragmentDurationPair fdp = fragmentDurationPairs[i]; if(fdp.duration == 0) { // some kind of discontinuity continue; } uint startFragmentId = fdp.firstFragment; uint startTime = (uint)fdp.durationAccrued; // find the valid entry or the next skip, gap, or skip+gap bool isLast = true; int j; for (j = i + 1; j < fragmentDurationPairs.Count; ++j) { if(fragmentDurationPairs[j].duration != 0 || // next is valid entry fragmentDurationPairs[j].discontinuityIndicator == 1 || // next is skip fragmentDurationPairs[j].discontinuityIndicator == 2 || // next is gap fragmentDurationPairs[j].discontinuityIndicator == 3) // next is skip+gap { isLast = false; break; } else { // eof or some unknown kind of discontinuity } } uint endFragmentId; uint endTime; if (isLast) { // there's no next entry endFragmentId = 0; endTime = 0; } else { endFragmentId = fragmentDurationPairs[j].firstFragment; if(startFragmentId > endFragmentId) // very uncommon case: fragment numbers are out of order continue; endTime = startTime + (endFragmentId - startFragmentId) * fdp.duration; } bool shouldContinue = f(fdp, isLast, startFragmentId, endFragmentId, startTime, endTime); if (!shouldContinue || isLast) return; } } } }