/*
* PS3 Media Server, for streaming any medias to your PS3.
* Copyright (C) 2008 A.Brochard
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; version 2
* of the License only.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package net.pms.encoders;
import net.pms.PMS;
import net.pms.io.*;
import net.pms.util.H264AnnexBInputStream;
import net.pms.util.PCMAudioOutputStream;
import net.pms.util.ProcessUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.util.ArrayList;
public class AviDemuxerInputStream extends InputStream {
private static final Logger logger = LoggerFactory.getLogger(AviDemuxerInputStream.class);
private Process process;
private InputStream stream;
private ArrayList<ProcessWrapper> attachedProcesses;
private long readCount = -1;
private String streamVideoTag;
private Track[] track = new Track[2];
private int numberOfAudioChannels;
private OutputStream aOut;
private OutputStream vOut;
private long audiosize;
private long videosize;
private InputStream realIS;
private Thread parsing;
private OutputParams params;
@Override
public void close() throws IOException {
if (process != null) {
ProcessUtil.destroy(process);
}
super.close();
}
public AviDemuxerInputStream(InputStream fin, final OutputParams params, ArrayList<ProcessWrapper> at) throws IOException {
stream = fin;
logger.trace("Opening AVI Stream");
this.attachedProcesses = at;
this.params = params;
aOut = params.output_pipes[1].getOutputStream();
if (params.no_videoencode && params.forceType != null && params.forceType.equals("V_MPEG4/ISO/AVC") && params.header != null) {
// NOT USED RIGHT NOW
PipedOutputStream pout = new PipedOutputStream();
final InputStream pin = new H264AnnexBInputStream(new PipedInputStream(pout), params.header);
final OutputStream out = params.output_pipes[0].getOutputStream();
Runnable r = new Runnable() {
public void run() {
try {
byte[] b = new byte[512 * 1024];
int n = -1;
while ((n = pin.read(b)) > -1) {
out.write(b, 0, n);
}
} catch (Exception e) {
logger.error(null, e);
}
}
};
pin.close();
vOut = pout;
new Thread(r, "Avi Demuxer").start();
} else {
vOut = params.output_pipes[0].getOutputStream();
}
Runnable r = new Runnable() {
public void run() {
try {
// TODO(tcox): Is this used anymore?
TsMuxeRVideo ts = new TsMuxeRVideo(PMS.getConfiguration());
File f = new File(PMS.getConfiguration().getTempFolder(), "pms-tsmuxer.meta");
PrintWriter pw = new PrintWriter(f);
pw.println("MUXOPT --no-pcr-on-video-pid --no-asyncio --new-audio-pes --vbr --vbv-len=500");
String videoType = "V_MPEG-2";
if (params.no_videoencode && params.forceType != null) {
videoType = params.forceType;
}
String fps = "";
if (params.forceFps != null) {
fps = "fps=" + params.forceFps + ", ";
}
String audioType = "A_LPCM";
if (params.lossyaudio) {
audioType = "A_AC3";
}
pw.println(videoType + ", \"" + params.output_pipes[0].getOutputPipe() + "\", " + fps + "level=4.1, insertSEI, contSPS, track=1");
pw.println(audioType + ", \"" + params.output_pipes[1].getOutputPipe() + "\", track=2");
pw.close();
PipeProcess tsPipe = new PipeProcess(System.currentTimeMillis() + "tsmuxerout.ts");
ProcessWrapper pipe_process = tsPipe.getPipeProcess();
attachedProcesses.add(pipe_process);
pipe_process.runInNewThread();
tsPipe.deleteLater();
String[] cmd = new String[]{ts.executable(), f.getAbsolutePath(), tsPipe.getInputPipe()};
ProcessBuilder pb = new ProcessBuilder(cmd);
process = pb.start();
ProcessWrapper pwi = new ProcessWrapperLiteImpl(process);
attachedProcesses.add(pwi);
// "Gob": a cryptic name for (e.g.) StreamGobbler - i.e. a stream
// consumer that reads and discards the stream
new Gob(process.getErrorStream()).start();
new Gob(process.getInputStream()).start();
realIS = tsPipe.getInputStream();
ProcessUtil.waitFor(process);
logger.trace("tsMuxeR muxing finished");
} catch (IOException e) {
logger.error(null, e);
}
}
};
Runnable r2 = new Runnable() {
public void run() {
try {
//Thread.sleep(500);
parseHeader();
} catch (IOException e) {
logger.debug("Parsing error", e);
}
}
};
logger.trace("Launching tsMuxeR muxing");
new Thread(r, "Avi Demuxer tsMuxeR").start();
parsing = new Thread(r2, "Avi Demuxer Header Parser");
logger.trace("Ready to mux");
}
private void parseHeader() throws IOException {
logger.trace("Parsing AVI stream");
String id = getString(stream, 4);
getBytes(stream, 4);
String type = getString(stream, 4);
if (!"RIFF".equalsIgnoreCase(id) || !"AVI ".equalsIgnoreCase(type)) {
throw new IOException("Not AVI file");
}
byte[] hdrl = null;
while (true) {
String command = getString(stream, 4);
int length = (readBytes(stream, 4) + 1) & ~1;
if ("LIST".equalsIgnoreCase(command)) {
command = getString(stream, 4);
length -= 4;
if ("movi".equalsIgnoreCase(command)) {
break;
}
if ("hdrl".equalsIgnoreCase(command)) {
hdrl = getBytes(stream, length);
}
if ("idx1".equalsIgnoreCase(command)) {
/*idx = */
getBytes(stream, length);
}
if ("iddx".equalsIgnoreCase(command)) {
/*idx = */
getBytes(stream, length);
}
} else {
getBytes(stream, length);
}
}
int streamNumber = 0;
int lastTagID = 0;
for (int i = 0; i < hdrl.length;) {
String command = new String(hdrl, i, 4);
int size = str2ulong(hdrl, i + 4);
if ("LIST".equalsIgnoreCase(command)) {
i += 12;
continue;
}
String command2 = new String(hdrl, i + 8, 4);
if ("strh".equalsIgnoreCase(command)) {
lastTagID = 0;
if ("vids".equalsIgnoreCase(command2)) {
String compressor = new String(hdrl, i + 12, 4);
int scale = str2ulong(hdrl, i + 28);
int rate = str2ulong(hdrl, i + 32);
track[0] = new Track(compressor, scale, rate, -1);
streamVideoTag = new String(new char[]{
(char) ((streamNumber / 10) + '0'),
(char) ((streamNumber % 10) + '0'), 'd', 'b'});
streamNumber++;
lastTagID = 1;
}
if ("auds".equalsIgnoreCase(command2)) {
int scale = str2ulong(hdrl, i + 28);
int rate = str2ulong(hdrl, i + 32);
int sampleSize = str2ulong(hdrl, i + 52);
track[1 + numberOfAudioChannels++] = new Track(null, scale, rate, sampleSize);
streamNumber++;
lastTagID = 2;
}
}
if ("strf".equalsIgnoreCase(command)) {
if (lastTagID == 1) {
byte[] information = new byte[size]; // formerly size-4
System.arraycopy(hdrl, i + 8, information, 0, information.length); // formerly i+4
track[0].setBih(information);
}
if (lastTagID == 2) {
byte[] information = new byte[size]; // formerly size-4
System.arraycopy(hdrl, i + 8, information, 0, information.length); // formerly i+4
Track aud = track[1 + numberOfAudioChannels - 1];
aud.setBih(information);
int bitsPerSample = str2ushort(information, 14);
aud.setBitsPerSample(bitsPerSample);
int nbAudio = str2ushort(information, 2);
aud.setNbAudio(nbAudio);
long fileLength = 100;
if (params.losslessaudio) {
aOut = new PCMAudioOutputStream(aOut, nbAudio, 48000, bitsPerSample);
}
if (!params.lossyaudio && params.losslessaudio) {
writePCMHeader(aOut, fileLength, nbAudio, aud.getRate(), aud.getSampleSize(), bitsPerSample);
}
}
}
if (size % 2 != 0) {
size++;
}
i += size + 8;
}
logger.trace("Found " + streamNumber + " stream(s)");
boolean init = false;
while (true) {
String command = null;
try {
command = getString(stream, 4);
} catch (Exception e) {
logger.trace("Error reading stream: " + e.getMessage());
break;
}
if (command == null) {
break;
}
command = command.toUpperCase();
int size = readBytes(stream, 4);
boolean framed = false;
while ("LIST".equals(command)
|| "RIFF".equals(command)
|| "JUNK".equals(command)) {
if (size < 0) {
size = 4;
}
getBytes(stream, "RIFF".equals(command) ? 4 : size);
command = getString(stream, 4).toUpperCase();
size = readBytes(stream, 4);
if (("LIST".equals(command) || "RIFF".equals(command) || "JUNK".equals(command)) && (size % 2 != 0)) {
readByte(stream);
}
}
String videoTag = streamVideoTag.substring(0, 3);
if (command.substring(0, 3).equalsIgnoreCase(videoTag) && (command.charAt(3) == 'B' || command.charAt(3) == 'C')) {
byte[] buffer = getBytes(stream, size);
if (!command.equalsIgnoreCase("IDX1")) {
vOut.write(buffer);
videosize += size;
}
framed = true;
}
if (!framed) {
for (int i = 0; i < numberOfAudioChannels; i++) {
byte[] buffer = getBytes(stream, size);
if (!command.equalsIgnoreCase("IDX1")) {
aOut.write(buffer, init ? 4 : 0, init ? (size - 4) : size);
init = false;
audiosize += size;
}
framed = true;
}
}
if (!framed) {
throw new IOException("Not header: " + command);
}
if (size % 2 != 0) {
readByte(stream);
}
}
logger.trace("output pipes closed");
aOut.close();
vOut.close();
}
private String getString(InputStream input, int sz) throws IOException {
byte[] bb = getBytes(input, sz);
return new String(bb);
}
private byte[] getBytes(InputStream input, int sz) throws IOException {
byte[] bb = new byte[sz];
int n = input.read(bb);
while (n < sz) {
int u = input.read(bb, n, sz - n);
if (u == -1) {
break;
}
n += u;
}
return bb;
}
private final int readBytes(InputStream input, int number) throws IOException {
byte[] buffer = new byte[number];
int read = input.read(buffer);
if (read < number) {
if (read < 0) {
throw new IOException("End of stream");
}
for (int i = read; i < number; i++) {
buffer[i] = (byte) readByte(input);
}
}
/**
* Create integer
*/
switch (number) {
case 1:
return (buffer[0] & 0xff);
case 2:
return (buffer[0] & 0xff) | ((buffer[1] & 0xff) << 8);
case 3:
return (buffer[0] & 0xff) | ((buffer[1] & 0xff) << 8)
| ((buffer[2] & 0xff) << 16);
case 4:
return (buffer[0] & 0xff) | ((buffer[1] & 0xff) << 8)
| ((buffer[2] & 0xff) << 16) | ((buffer[3] & 0xff) << 24);
default:
throw new IOException("Illegal Read quantity");
}
}
private final int readByte(InputStream input) throws IOException {
return input.read();
}
public static final int str2ulong(byte[] data, int i) {
return (data[i] & 0xff) | ((data[i + 1] & 0xff) << 8)
| ((data[i + 2] & 0xff) << 16) | ((data[i + 3] & 0xff) << 24);
}
public static final int str2ushort(byte[] data, int i) {
return (data[i] & 0xff) | ((data[i + 1] & 0xff) << 8);
}
public static final byte[] getLe32(long value) {
byte[] buffer = new byte[4];
buffer[0] = (byte) (value & 0xff);
buffer[1] = (byte) ((value >> 8) & 0xff);
buffer[2] = (byte) ((value >> 16) & 0xff);
buffer[3] = (byte) ((value >> 24) & 0xff);
return buffer;
}
public static final byte[] getLe16(int value) {
byte[] buffer = new byte[2];
buffer[0] = (byte) (value & 0xff);
buffer[1] = (byte) ((value >> 8) & 0xff);
return buffer;
}
@Override
public int read() throws IOException {
if (readCount == -1) {
parsing.start();
readCount = 0;
}
int c = 0;
while ((realIS == null || videosize == 0 || audiosize == 0) && c < 15) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
logger.trace("Sleep interrupted", e);
}
c++;
}
if (realIS != null) {
readCount++;
return realIS.read();
} else {
return -1;
}
}
@Override
public int read(byte[] b) throws IOException {
if (readCount == -1) {
parsing.start();
readCount = 0;
}
int c = 0;
while ((realIS == null || videosize == 0 || audiosize == 0) && c < 15) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
logger.trace("Sleep interrupted", e);
}
c++;
}
if (realIS != null) {
int n = realIS.read(b);
readCount += n;
return n;
} else {
return -1;
}
}
public static void writePCMHeader(OutputStream aOut, long fileLength, int nbAudio, int rate, int sampleSize, int bitsPerSample) throws IOException { }
}