package clearvolume.volume.sink.timeshift; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import java.util.TreeMap; import java.util.TreeSet; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import clearvolume.ClearVolumeCloseable; import clearvolume.volume.Volume; import clearvolume.volume.VolumeManager; import clearvolume.volume.sink.relay.RelaySinkAdapter; import clearvolume.volume.sink.relay.RelaySinkInterface; public class TimeShiftingSink extends RelaySinkAdapter implements RelaySinkInterface, ClearVolumeCloseable { private static final float cCleanUpRatio = 0.25f; private static final ExecutorService mSeekingExecutor = Executors.newSingleThreadExecutor(); private final SwitchableSoftReferenceManager<Volume> mSwitchableSoftReferenceManager; private final Object mLock = new Object(); private HashMap<Integer, TreeMap<Long, SwitchableSoftReference<Volume>>> mChannelToVolumeListsMap = new HashMap<>(); private final TreeSet<Integer> mAvailableChannels = new TreeSet<>(); private volatile long mSoftMemoryHorizonInTimePointIndices; private volatile long mHardMemoryHorizonInTimePointIndices; private volatile long mCleanUpPeriodInTimePoints; private volatile long mHighestTimePointIndexSeen = 0; private volatile long mTimeShift = 0; private volatile boolean mIsPlaying = true; public TimeShiftingSink(long pSoftMemoryHoryzonInTimePointIndices, long pHardMemoryHoryzonInTimePointIndices) { super(); mSwitchableSoftReferenceManager = new SwitchableSoftReferenceManager<>(); mSoftMemoryHorizonInTimePointIndices = Math.min(pSoftMemoryHoryzonInTimePointIndices, pHardMemoryHoryzonInTimePointIndices); mHardMemoryHorizonInTimePointIndices = Math.max(pHardMemoryHoryzonInTimePointIndices, pSoftMemoryHoryzonInTimePointIndices); mCleanUpPeriodInTimePoints = (long) (mSoftMemoryHorizonInTimePointIndices * cCleanUpRatio); } public void setTimeShiftNormalized(final double pTimeShiftNormalized) { final Runnable lRunnable = new Runnable() { @Override public void run() { synchronized (mLock) { final long lPreviousTimeShift = mTimeShift; // find the available data interval to evade invalid indices final long startPos = Math.max( 0, mHighestTimePointIndexSeen - mHardMemoryHorizonInTimePointIndices); final long interval = mHighestTimePointIndexSeen - startPos; // System.err.println("interval=[" + startPos +"," // +interval+"]"); mTimeShift = -Math.round(interval * pTimeShiftNormalized); if (lPreviousTimeShift != mTimeShift) for (final int lChannel : mAvailableChannels) sendVolumeInternal(lChannel); } } }; mSeekingExecutor.execute(lRunnable); } public void setTimeShift(long pTimeShift) { mTimeShift = pTimeShift; } public long getTimeShift() { return mTimeShift; } public long getHardMemoryHorizon() { return mHardMemoryHorizonInTimePointIndices; } public long getSoftMemoryHorizon() { return mSoftMemoryHorizonInTimePointIndices; } public long getNumberOfTimepoints() { return mHighestTimePointIndexSeen; } public int getNumberOfAvailableChannels() { return mAvailableChannels.size(); } public int getAvailableChannels() { return mAvailableChannels.size(); } @Override public void sendVolume(Volume pVolume) { synchronized (mLock) { final int lVolumeChannelID = pVolume.getChannelID(); mAvailableChannels.add(lVolumeChannelID); TreeMap<Long, SwitchableSoftReference<Volume>> lTimePointIndexToVolumeMapReference = mChannelToVolumeListsMap.get(lVolumeChannelID); if (lTimePointIndexToVolumeMapReference == null) { lTimePointIndexToVolumeMapReference = new TreeMap<>(); mChannelToVolumeListsMap.put( lVolumeChannelID, lTimePointIndexToVolumeMapReference); } lTimePointIndexToVolumeMapReference.put(pVolume.getTimeIndex(), wrapWithReference(pVolume)); mHighestTimePointIndexSeen = Math.max( mHighestTimePointIndexSeen, pVolume.getTimeIndex()); if (mIsPlaying) sendVolumeInternal(lVolumeChannelID); cleanUpOldVolumes( mHighestTimePointIndexSeen, lVolumeChannelID); } } private void sendVolumeInternal(int lVolumeChannelID) { synchronized (mLock) { final Volume lVolumeToSend = getVolumeToSend(lVolumeChannelID); if (lVolumeToSend != null) { getRelaySink().sendVolume(lVolumeToSend); } else { System.err.println("Did not have any volume to send :("); } cleanUpOldVolumes( mHighestTimePointIndexSeen, lVolumeChannelID); } } private SwitchableSoftReference<Volume> wrapWithReference(final Volume pVolume) { final Runnable lCleaningRunnable = new Runnable() { @Override public void run() { System.out.println("CLEANING!"); pVolume.makeAvailableToManager(); } }; return mSwitchableSoftReferenceManager.wrapReference( pVolume, lCleaningRunnable); } private Volume getVolumeToSend(int pVolumeChannelID) { synchronized (mLock) { final TreeMap<Long, SwitchableSoftReference<Volume>> lTimePointIndexToVolumeMap = mChannelToVolumeListsMap.get(pVolumeChannelID); if (lTimePointIndexToVolumeMap.isEmpty()) return null; Entry<Long, SwitchableSoftReference<Volume>> lIndexVolumeEntry = lTimePointIndexToVolumeMap.floorEntry(mHighestTimePointIndexSeen + mTimeShift); if (lIndexVolumeEntry == null) lIndexVolumeEntry = lTimePointIndexToVolumeMap.ceilingEntry(mHighestTimePointIndexSeen + mTimeShift); if (lIndexVolumeEntry == null) return null; final Volume lVolume = lIndexVolumeEntry.getValue().get(); if (lVolume == null) { lTimePointIndexToVolumeMap.remove(lIndexVolumeEntry.getKey()); return getVolumeToSend(pVolumeChannelID); } return lVolume; } } private void cleanUpOldVolumes( long pTimePointIndex, int pChannelID) { synchronized (mLock) { if (pTimePointIndex % mCleanUpPeriodInTimePoints != 0) return; final TreeMap<Long, SwitchableSoftReference<Volume>> lTimePointIndexToVolumeMap = mChannelToVolumeListsMap.get(pChannelID); if (lTimePointIndexToVolumeMap.isEmpty()) { mChannelToVolumeListsMap.remove(pChannelID); return; } final Long lLastTimePoint = lTimePointIndexToVolumeMap.lastKey(); Long lTimePoint = lLastTimePoint; while (lTimePoint != null && lTimePoint > lLastTimePoint - mSoftMemoryHorizonInTimePointIndices) { lTimePoint = lTimePointIndexToVolumeMap.lowerKey(lTimePoint); } while (lTimePoint != null && lTimePoint > lLastTimePoint - mHardMemoryHorizonInTimePointIndices) { final SwitchableSoftReference<Volume> lSwitchableSoftReference = lTimePointIndexToVolumeMap.get(lTimePoint); if (lSwitchableSoftReference.isGone()) { lTimePointIndexToVolumeMap.remove(lTimePoint); continue; } lSwitchableSoftReference.soften(); lTimePoint = lTimePointIndexToVolumeMap.lowerKey(lTimePoint); } while (lTimePoint != null) { final SwitchableSoftReference<Volume> lSwitchableSoftReference = lTimePointIndexToVolumeMap.get(lTimePoint); final Volume lVolume = lSwitchableSoftReference.get(); if (lVolume != null) lVolume.makeAvailableToManager(); lTimePointIndexToVolumeMap.remove(lTimePoint); lTimePoint = lTimePointIndexToVolumeMap.lowerKey(lTimePoint); } } } @Override public VolumeManager getManager() { return getRelaySink().getManager(); } @Override public void close() { synchronized (mLock) { for (final Map.Entry<Integer, TreeMap<Long, SwitchableSoftReference<Volume>>> lTimeLineForChannelEntry : mChannelToVolumeListsMap.entrySet()) { final TreeMap<Long, SwitchableSoftReference<Volume>> lTimeLineTreeMap = lTimeLineForChannelEntry.getValue(); for (final Map.Entry<Long, SwitchableSoftReference<Volume>> lTimePointEntry : lTimeLineTreeMap.entrySet()) { final SwitchableSoftReference<Volume> lVolumeSoftReference = lTimePointEntry.getValue(); final Volume lVolume = lVolumeSoftReference.get(); if (lVolume != null) lVolume.close(); lVolumeSoftReference.soften(); } lTimeLineTreeMap.clear(); } mChannelToVolumeListsMap.clear(); mChannelToVolumeListsMap = null; mAvailableChannels.clear(); } } public void pause() { mIsPlaying = false; } public void play() { mIsPlaying = true; } }