package se.sics.gvod.ls.video;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.URL;
import java.util.*;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.logging.Level;
import java.util.logging.Logger;
import se.sics.gvod.ls.http.HTTPStreamingClient;
import se.sics.gvod.ls.http.HTTPStreamingServer;
import se.sics.gvod.ls.system.LSConfig;
import se.sics.gvod.ls.system.PieceHandler;
import se.sics.gvod.ls.video.snapshot.SimulationSingleton;
import se.sics.gvod.ls.video.snapshot.VideoStats;
import se.sics.gvod.video.msgs.EncodedSubPiece;
import se.sics.gvod.video.msgs.Piece;
import se.sics.gvod.video.msgs.SubPiece;
/**
* Handles input and output of data to the Video component. In the source it
* reads from various resources; file system, HTTP, etc. and converts to data
* for dissemination. In peers receiving data it organizes and outputs the data
* to an output stream.
*
* @author Niklas Wahlén <nwahlen@kth.se>
*/
public class VideoIO implements Runnable {
private Map<Integer, VideoFEC> fecs; // piece id, fec
private Map<Integer, Piece> pieceBuffer; // piece id, piece
private Video video;
// source
private HTTPStreamingClient stream;
// downloader
private HTTPStreamingServer server;
private Map<Integer, Long> pieceStartTimes; // piece id, start time for dl
private Set<Integer> completePieces;
private Set<Integer> skippedPieces;
private int currentPieceId, maxStreamRate, bufferLength, missedPieces;
private LinkedBlockingQueue<SubPiece> playBackQueue; // this structure is ordered, while the piece buffer may not be
public VideoIO(Video video) {
this.video = video;
fecs = new HashMap<Integer, VideoFEC>();
pieceBuffer = new HashMap<Integer, Piece>();
pieceStartTimes = new HashMap<Integer, Long>();
completePieces = new HashSet<Integer>();
skippedPieces = new HashSet<Integer>();
currentPieceId = -2;
playBackQueue = new LinkedBlockingQueue<SubPiece>();
if (LSConfig.isSimulation()) {
maxStreamRate = 66 * SubPiece.SUBPIECE_DATA_SIZE;
} else {
maxStreamRate = Integer.MAX_VALUE;
}
}
// calculates the current PieceID.
// Next piece should have been decoded or
public void cycle() {
long currentTime = System.currentTimeMillis();
// handle completed pieces and prepare for playback
Set<Integer> toRemoveComplete = new HashSet<Integer>();
int streamLag = 0;
long oldestTime = Long.MAX_VALUE;
while (pieceBuffer.containsKey(currentPieceId) || skippedPieces.contains(currentPieceId)) {
if (pieceBuffer.containsKey(currentPieceId)) {
playBackQueue.addAll(Arrays.asList(pieceBuffer.get(currentPieceId).getSubPieces()));
if (LSConfig.isSimulation()) {
Long pieceStartTime = SimulationSingleton.getInstance().get(currentPieceId);
if ((pieceStartTime != null) && ((currentTime - pieceStartTime) > streamLag)) {
streamLag = (int) (currentTime - pieceStartTime);
}
}
toRemoveComplete.add(currentPieceId);
}
Long startTime = pieceStartTimes.get(currentPieceId);
if (startTime < oldestTime) {
oldestTime = startTime;
}
currentPieceId++;
}
if (LSConfig.isSimulation()) {
if (streamLag > 0) {
VideoStats.instance(video.getSelf()).setStreamLag(streamLag);
}
}
int streamRate = 0;
while (!playBackQueue.isEmpty() && (streamRate < maxStreamRate)) {
streamRate += SubPiece.SUBPIECE_DATA_SIZE;
SubPiece piece = playBackQueue.poll();
// Check if the system is configured to deliver a HTTP stream
if (LSConfig.hasDestUrlSet()) {
// Deliver to HTTP Server
this.deliver(piece);
}
}
// look for pieces which are exceeding buffer limit
Set<Integer> toRemoveMissing = new HashSet<Integer>();
missedPieces = 0;
for (Integer id : fecs.keySet()) {
Long startTime = pieceStartTimes.get(id);
if ((currentTime - startTime) > LSConfig.VIDEO_MAX_BUFFER_SIZE) {
skippedPieces.add(id);
toRemoveMissing.add(id);
missedPieces++;
} else if (startTime < oldestTime) {
oldestTime = startTime;
}
}
// clean
for (Integer id : toRemoveComplete) {
pieceBuffer.remove(id);
// send removal signal to gossip
video.triggerRemoval(id);
}
for (Integer id : toRemoveMissing) {
fecs.remove(id);
// send removal signal to gossip
video.triggerRemoval(id);
}
// stats
VideoStats.instance(video.getSelf()).setStreamRate(streamRate);
VideoStats.instance(video.getSelf()).setMissedPieces(toRemoveMissing.size());
if (oldestTime < Long.MAX_VALUE) {
bufferLength = (int) (currentTime - oldestTime);
VideoStats.instance(video.getSelf()).setBufferLength(bufferLength);
}
}
/*
* SOURCE -- HANDLE EXTERNAL INPUT
*/
public void setSource(URL url) throws IOException {
stream = new HTTPStreamingClient(url);
}
public void setSource(File file) throws IOException {
stream = new HTTPStreamingClient(file);
}
@Override
public void run() {
if (LSConfig.isSimulation()) {
stream.run();
} else {
new Thread(stream).start();
}
while (stream.isReading() || stream.hasNextPiece()) {
while (!stream.hasNextPiece());
Piece piece = stream.getNextPiece();
encodeAndAdvertise(piece);
}
}
/*
* LEECHER -- HANDLE ENCODED SUB-PIECES
*/
public void startServer(InetSocketAddress serverAddress) throws IOException {
server = new HTTPStreamingServer(serverAddress);
}
public void handleEncodedSubPiece(EncodedSubPiece p) {
// If the piece is among the completed pieces no further action required
if (completePieces.contains(p.getParentId())
|| skippedPieces.contains(p.getParentId())) {
return;
}
VideoFEC fec = fecs.get(p.getParentId());
if (fec == null) {
fec = new VideoFEC(p.getParentId());
fecs.put(p.getParentId(), fec);
pieceStartTimes.put(p.getParentId(), System.currentTimeMillis());
}
if (!fec.isReady() && !fec.contains(p)) {
fec.addEncodedSubPiece(p);
if (fec.isReady() && !fec.isDecoded()) {
// Make sure no more (encoded) sub-pieces of this piece are
// requested this is easiest achieved by decoding this piece,
// encoding it again and then adding the sub-pieces to the
// VideoGossip. (This is done through encode().)
Piece piece = fec.decode();
completePieces.add(piece.getId());
pieceBuffer.put(piece.getId(), piece);
VideoStats.instance(video.getSelf()).setCompletedPiece(piece.getId());
this.encodeAndAdvertise(piece);
//
// Remove this FEC from the Map
fecs.remove(fec.getPiece().getId());
}
}
// if first received piece
if (currentPieceId < 0) {
currentPieceId = p.getParentId();
}
}
/**
* Method called when a new Piece is ready. The piece is encoded using FEC
* and the encoded sub-pieces advertised to the node's neighbours.
*
* @param piece The Piece to be encoded and published (advertised)
* @see VideoPieceHandler
* @see BlockBasedFEC
*/
private void encodeAndAdvertise(Piece piece) {
// Encode piece
VideoFEC fec = new VideoFEC(piece);
for (int i = 0; i < fec.getEncodedSubPieces().length; i++) {
video.publish(fec.getEncodedSubPiece(i));
}
}
private void deliver(SubPiece sp) {
try {
server.deliver(sp);
} catch (IOException ex) {
Logger.getLogger(VideoIO.class.getName()).log(Level.SEVERE, null, ex);
}
}
private void deliver(Piece piece) {
try {
server.deliver(piece);
} catch (IOException ex) {
Logger.getLogger(VideoIO.class.getName()).log(Level.SEVERE, null, ex);
}
}
public void close() {
if (server != null) {
server.stop();
}
}
public boolean hasPiece(int id) {
if (id < 0) {
return true;
}
return completePieces.contains(id);
}
/*
* Write Piece data to file -- mostly used for testing purposes
*/
public void writePieceData(String filePath) throws IOException {
// TODO: never assume that the pieces are ordered at method invocation
boolean append = false;
System.out.println(PieceHandler.class.getSimpleName() + ": About to write " + pieceBuffer.size() + " pieces.");
FileOutputStream out = getFileOutputStream(filePath, append);
for (int i = 0; i < pieceBuffer.size(); i++) {
Piece p = pieceBuffer.get(i);
if (p.getId() != i) {
System.out.println("Piece at index " + i + " has Id " + p.getId());
}
if (i < pieceBuffer.size() - 1) {
for (SubPiece sp : p.getSubPieces()) {
out.write(sp.getData());
}
} else {
// Find the location of padding bytes
int found = 0;
byte[] data = new byte[Piece.PIECE_DATA_SIZE];
SubPiece[] sps = p.getSubPieces();
// First gather all bytes into a single array, in case the
// padding code was cut into two separate sub pieces
for (int n = 0, j = 0; n < sps.length; n++, j += SubPiece.SUBPIECE_DATA_SIZE) {
System.arraycopy(sps[n].getData(), 0, data, j, SubPiece.SUBPIECE_DATA_SIZE);
}
for (int j = 0; j < data.length - Piece.PADDING_CODE.length; j++) {
if (data[j] == Piece.PADDING_CODE[0]
&& data[j + 1] == Piece.PADDING_CODE[1]
&& data[j + 2] == Piece.PADDING_CODE[2]
&& data[j + 3] == Piece.PADDING_CODE[3]) {
System.out.println(PieceHandler.class.getSimpleName() + ": Detected padding code starting at " + p.getId() + "[" + j + "]");
break;
}
// if the loop is still running the byte is valid data
out.write(data[j]);
}
}
}
out.flush();
out.close();
}
private static FileOutputStream getFileOutputStream(String filePath, boolean append) throws FileNotFoundException {
return new FileOutputStream(createFile(filePath), append);
}
private static File createFile(String filePath) {
File file = new File(filePath);
if (file.exists()) {
file.delete();
}
try {
file.createNewFile();
} catch (IOException ex) {
Logger.getLogger(PieceHandler.class.getName()).log(Level.SEVERE, null, ex);
}
System.out.println(PieceHandler.class.getSimpleName() + ": Created new file for writing: " + file.getName());
return file;
}
public static Comparator<Piece> pieceComparator = new Comparator<Piece>() {
@Override
public int compare(Piece t, Piece t1) {
if (t == null && t1 == null) {
return 0;
} else if (t == null) {
return 1;
} else if (t1 == null) {
return -1;
} else if (t.getId() < t1.getId()) {
return -1;
} else if (t.getId() > t1.getId()) {
return 1;
} else {
return 0;
}
}
};
public int getCurrentBufferLength() {
return bufferLength;
}
public int getMissedPieces() {
return missedPieces;
}
}