package se.sics.gvod.ls.video; import java.io.File; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.URL; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.logging.Level; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import se.sics.asdistances.ASDistances; import se.sics.gvod.common.RetryComponentDelegator; import se.sics.gvod.common.Self; import se.sics.gvod.croupier.PeerSamplePort; import se.sics.gvod.croupier.events.CroupierSample; import se.sics.gvod.ls.interas.InterAsPort; import se.sics.gvod.ls.interas.events.InterAsSample; import se.sics.gvod.ls.system.LSConfig; import se.sics.gvod.ls.video.events.VideoCycle; import se.sics.gvod.ls.video.snapshot.SimulationSingleton; import se.sics.gvod.ls.video.snapshot.StatsRestClient; import se.sics.gvod.ls.video.snapshot.VideoStats; import se.sics.gvod.nat.common.MsgRetryComponent; import se.sics.gvod.net.VodAddress; import se.sics.gvod.timer.CancelPeriodicTimeout; import se.sics.gvod.timer.SchedulePeriodicTimeout; import se.sics.gvod.timer.ScheduleTimeout; import se.sics.gvod.timer.Timeout; import se.sics.gvod.timer.TimeoutId; import se.sics.gvod.video.msgs.EncodedSubPiece; import se.sics.gvod.video.msgs.VideoConnectionMsg; import se.sics.gvod.video.msgs.VideoConnectionMsg.Disconnect; import se.sics.gvod.video.msgs.VideoPieceMsg; import se.sics.gvod.video.msgs.VideoPieceMsg.Advertisement; import se.sics.gvod.video.msgs.VideoPieceMsg.Request; import se.sics.gvod.video.msgs.VideoPieceMsg.RequestTimeout; import se.sics.gvod.video.msgs.VideoPieceMsg.Response; import se.sics.kompics.Handler; import se.sics.kompics.Positive; import se.sics.kompics.Stop; /** * * @author Niklas Wahlén <nwahlen@kth.se> */ public class Video extends MsgRetryComponent { // Framework private final Logger logger = LoggerFactory.getLogger(Video.class); private String compName; private Self self; private Positive<InterAsPort> interAs = positive(InterAsPort.class); private Positive<PeerSamplePort> croupier = positive(PeerSamplePort.class); // Child classes private VideoNeighbours neighbours; private VideoGossip gossiping; private VideoIO io; private Map<Integer, EncodedSubPiece> subPieceBuffer; // Video component configuration private long videoCyclePeriod = LSConfig.VIDEO_CYCLE; private TimeoutId videoCycleTimeoutId; private Set<TimeoutId> gossipTimeoutIds; private boolean source; // Video simulation private int warmup; private int currentEncodedSubPiece = 0; private boolean fileWritten = false; // Monitoring and diagnostics private StatsRestClient statsClient; public static int SYSTEM_VIDEO_OVERLAY_ID = 1431; private int roundCount = 0; public Video() { this(null); } public Video(RetryComponentDelegator delegator) { super(delegator); this.delegator.doAutoSubscribe(); io = new VideoIO(this); subPieceBuffer = new HashMap<Integer, EncodedSubPiece>(); gossipTimeoutIds = new HashSet<TimeoutId>(); if (LSConfig.hasMonitorUrlSet()) { statsClient = new StatsRestClient(LSConfig.getMonitorServerUrl()); } } Handler<VideoInit> handleInit = new Handler<VideoInit>() { @Override public void handle(VideoInit init) { self = init.getSelf(); // Configuration warmup = LSConfig.VIDEO_WARMUP; source = init.isSource(); neighbours = new VideoNeighbours(delegator, self, network, source); gossiping = new VideoGossip(delegator, self, network, timer, neighbours, subPieceBuffer, gossipTimeoutIds, source); SchedulePeriodicTimeout periodicTimeout = new SchedulePeriodicTimeout(0, videoCyclePeriod); periodicTimeout.setTimeoutEvent(new VideoCycle(periodicTimeout)); videoCycleTimeoutId = periodicTimeout.getTimeoutEvent().getTimeoutId(); delegator.doTrigger(periodicTimeout, timer); VideoStats.addNode(self.getAddress(), source); if (LSConfig.hasMonitorUrlSet()) { VideoStats.instance(self).setExperimentId(LSConfig.getExperimentId()); VideoStats.instance(self).setExperimentIteration(LSConfig.getExperimentIteration()); } int asn = ASDistances.getInstance().getASFromIP(self.getIp().getHostAddress()); VideoStats.instance(self).setAsn(asn); compName = "Video(" + self.getId() + ") "; if (source) { logger.info(self.getId() + ": Source created"); compName = "Video(" + self.getId() + " SOURCE) "; try { if (LSConfig.hasInputFileSet()) { io.setSource(new File(LSConfig.getInputFilename())); } else if (LSConfig.hasSourceUrlSet()) { io.setSource(new URL(LSConfig.getSourceUrl())); } else { logger.error("Neither input file nor source URL specified for source"); System.exit(1); } if (LSConfig.isSimulation()) { logger.info("Run video io"); io.run(); logger.info("Run video io: done"); } else { new Thread(io).start(); } } catch (IOException ex) { java.util.logging.Logger.getLogger(Video.class.getName()).log(Level.SEVERE, null, ex); } } else { // Not source try { if (LSConfig.hasDestUrlSet()) { String ip = LSConfig.getDestIp(); InetAddress addr = InetAddress.getByName(LSConfig.getDestIp()); int port = LSConfig.getDestPort(); io.startServer(new InetSocketAddress(port)); } } catch (IOException ex) { java.util.logging.Logger.getLogger(Video.class.getName()).log(Level.SEVERE, null, ex); } neighbours.incRandom(true); } } }; /* * Node samples */ Handler<InterAsSample> handleInterAsSample = new Handler<InterAsSample>() { @Override public void handle(InterAsSample sample) { neighbours.handleInterAsSample(sample); } }; Handler<CroupierSample> handleCroupierSample = new Handler<CroupierSample>() { @Override public void handle(CroupierSample sample) { neighbours.handleCroupierSample(sample); } }; /* * Cycle */ Handler<VideoCycle> handleCycle = new Handler<VideoCycle>() { @Override public void handle(VideoCycle c) { // Warmup for simulation. Croupier takes time to deliver samples, // TODO - should remove this for production. if (warmup > 0) { // send additional connection requests during warmup neighbours.sendInterAsConnectionRequests(1); warmup--; } else { if (!source && (neighbours.getIngoingConnections() == 0)) { neighbours.mildPanic(); } if (!source && LSConfig.hasMonitorUrlSet()) { statsClient.createStats(VideoStats.instance(self)); } } updateRandomNeighbours(); neighbours.cycle(); io.cycle(); if (!neighbours.isEmpty()) { gossiping.cycle(); if (LSConfig.isSimulation()) { if (source) { Set<Integer> newPieces = new HashSet<Integer>(); int interval = 5 * 14; // encoded sub-pieces // rate = interval * 1316 B int from = currentEncodedSubPiece; int to = currentEncodedSubPiece + interval; for (int i = from; i < to; i++) { // if we run out of subpieces, restart from the beginning // (however publish with increasing piece and sub-piece ids) EncodedSubPiece esp = subPieceBuffer.get(i % subPieceBuffer.size()); int ppieceId = (i - esp.getEncodedIndex()) / LSConfig.FEC_ENCODED_PIECES; EncodedSubPiece pesp = new EncodedSubPiece(i, esp.getEncodedIndex(), esp.getData(), ppieceId); gossiping.publish(pesp); if (!newPieces.contains(pesp.getParentId())) { newPieces.add(pesp.getParentId()); } } for (Integer newPieceId : newPieces) { SimulationSingleton.getInstance().register(newPieceId); } currentEncodedSubPiece += interval; } if (LSConfig.hasOutputFileSet() && !fileWritten && (subPieceBuffer.size() == 33 * LSConfig.FEC_ENCODED_PIECES)) { // dissemination ended try { String fileName = source ? "streamSource.mp4" : LSConfig.getOutputFilename() + self.getId() + ".mp4"; io.writePieceData(fileName); } catch (IOException ex) { java.util.logging.Logger.getLogger(Video.class.getName()).log(Level.SEVERE, null, ex); } fileWritten = true; } } } // endif !neighbours.isEmpty() roundCount++; } }; /* * Neighbour connections */ Handler<VideoConnectionMsg.Request> handleConnectionRequest = new Handler<VideoConnectionMsg.Request>() { @Override public void handle(VideoConnectionMsg.Request request) { neighbours.handleConnectionRequest(request); neighbours.updateTimstamp(request.getVodSource()); } }; Handler<VideoConnectionMsg.RequestTimeout> handleConnectionRequestTimeout = new Handler<VideoConnectionMsg.RequestTimeout>() { @Override public void handle(VideoConnectionMsg.RequestTimeout timeout) { neighbours.handleConnectionRequestTimeout(timeout); } }; Handler<VideoConnectionMsg.Response> handleConnectionResponse = new Handler<VideoConnectionMsg.Response>() { @Override public void handle(VideoConnectionMsg.Response response) { neighbours.handleConnectionResponse(response); neighbours.updateTimstamp(response.getVodSource()); } }; Handler<VideoConnectionMsg.Disconnect> handleDisconnection = new Handler<VideoConnectionMsg.Disconnect>() { @Override public void handle(Disconnect disconnect) { logger.debug(compName + " received disconnection msg from " + disconnect.getVodSource()); neighbours.handleDisconnetion(disconnect); neighbours.updateTimstamp(disconnect.getVodSource()); } }; /* * Three-phase gossip */ Handler<VideoPieceMsg.Advertisement> handleAdvertisement = new Handler<VideoPieceMsg.Advertisement>() { @Override public void handle(Advertisement advertisement) { if (!source) { gossiping.handleAdvertisement(advertisement); neighbours.updateTimstamp(advertisement.getVodSource()); } } }; Handler<VideoPieceMsg.Request> handleRequest = new Handler<VideoPieceMsg.Request>() { @Override public void handle(Request request) { gossiping.handleRequest(request); neighbours.updateTimstamp(request.getVodSource()); } }; Handler<VideoPieceMsg.RequestTimeout> handleRequestTimeout = new Handler<VideoPieceMsg.RequestTimeout>() { @Override public void handle(RequestTimeout rt) { gossiping.handleTimeout(rt); } }; Handler<VideoPieceMsg.Response> handlePieces = new Handler<VideoPieceMsg.Response>() { @Override public void handle(Response response) { if (!source) { incDownloaded(response.getVodSource()); EncodedSubPiece p = response.getEncodedSubPiece(); io.handleEncodedSubPiece(p); // updateRandomNeighbours(p); gossiping.handlePieces(response); neighbours.updateTimstamp(response.getVodSource()); } } }; Handler<RemovalTimer> handleRemoval = new Handler<RemovalTimer>() { @Override public void handle(RemovalTimer removal) { if (!source) { int startGlobalId = removal.getPieceId() * LSConfig.FEC_ENCODED_PIECES; int endGlobalId = startGlobalId + LSConfig.FEC_ENCODED_PIECES - 1; for (int i = startGlobalId; i < endGlobalId; i++) { subPieceBuffer.remove(i); } } } }; public void publish(EncodedSubPiece esp) { if (LSConfig.isSimulation() && source) { // Put in buffer for use at cycle event (as in simulation, it is reading the data too quickly). subPieceBuffer.put(esp.getGlobalId(), esp); } else { gossiping.handlePiece(esp); } } /** * Updates the ratio of random neighbours depending on the current stream * quality. * * @param p */ private void updateRandomNeighbours() { float minimumBufferLengthInSeconds = 4.0f; // allow buffer to fill upp during first rounds // if ((p.getParentId() - 5) < 0) { // return; // } float currentBufferLength = io.getCurrentBufferLength() / LSConfig.VIDEO_CYCLE; if ((gossiping.getDownloaded() == 0) || (io.getMissedPieces() > 0) || (currentBufferLength < minimumBufferLengthInSeconds)) { neighbours.incRandom(true); } else { neighbours.incRandom(false); } } private void incDownloaded(VodAddress a) { short distance = neighbours.getDistanceTo(a); if (distance == 0) { VideoStats.instance(self).incDownloadedSubPiecesIntraAs(); } else if (distance == 1) { VideoStats.instance(self).incDownloadedSubPiecesNeighbourAs(); } else { VideoStats.instance(self).incDownloadedSubPiecesOtherAs(); } } @Override public void stop(Stop event) { if (videoCycleTimeoutId != null) { CancelPeriodicTimeout cancelTimeout = new CancelPeriodicTimeout(videoCycleTimeoutId); delegator.doTrigger(cancelTimeout, timer); } for (TimeoutId tid : gossipTimeoutIds) { CancelPeriodicTimeout cancelTimeout = new CancelPeriodicTimeout(tid); delegator.doTrigger(cancelTimeout, timer); } io.close(); } public Self getSelf() { return self; } /** * Called when a piece is ready or skipped -- schedules the removal of its * subpieces. (they may still be needed for gossiping for a while) * * @param id */ public void triggerRemoval(Integer id) { // maximum time until piece is not needed long delay = LSConfig.VIDEO_PIECE_REQUEST_RETRIES * LSConfig.VIDEO_PIECE_REQUEST_TIMEOUT; ScheduleTimeout st = new ScheduleTimeout(delay); st.setTimeoutEvent(new RemovalTimer(st, id)); delegator.doTrigger(st, timer); } public int getRoundCount() { return roundCount; } public static final class RemovalTimer extends Timeout { private final int pieceId; public RemovalTimer(ScheduleTimeout st, int pieceId) { super(st); this.pieceId = pieceId; } public int getPieceId() { return pieceId; } } }