001package net.bramp.ffmpeg.nut;
002
003import static com.google.common.base.Preconditions.checkNotNull;
004import static net.bramp.ffmpeg.nut.Packet.Startcode;
005
006import com.google.common.base.Charsets;
007import java.io.IOException;
008import java.io.InputStream;
009import java.util.ArrayList;
010import java.util.Arrays;
011import java.util.List;
012
013/**
014 * Demuxer for the FFmpeg Nut file format.
015 *
016 * <p>Lots of things not implemented, startcode searching, crc checks, etc
017 *
018 * @see <a
019 *     href="https://www.ffmpeg.org/~michael/nut.txt">https://www.ffmpeg.org/~michael/nut.txt</a>
020 * @see <a
021 *     href="https://github.com/FFmpeg/FFmpeg/blob/master/libavformat/nutdec.c">https://github.com/FFmpeg/FFmpeg/blob/master/libavformat/nutdec.c</a>
022 */
023public class NutReader {
024
025  // HEADER is the string "nut/multimedia container\0"
026  static final byte[] HEADER = {
027    0x6e, 0x75, 0x74, 0x2f, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x6d, 0x65, 0x64, 0x69, 0x61, 0x20, 0x63,
028    0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x00
029  };
030
031  public MainHeaderPacket header;
032  public final List<Stream> streams = new ArrayList<>();
033
034  final NutDataInputStream in;
035  final NutReaderListener listener;
036
037  public NutReader(InputStream in, NutReaderListener listener) {
038    this.in = new NutDataInputStream(in);
039    this.listener = checkNotNull(listener);
040  }
041
042  public static boolean isKnownStartcode(long startcode) {
043    return Startcode.of(startcode) != null;
044  }
045
046  /**
047   * Read the magic at the beginning of the file.
048   *
049   * @throws IOException If a I/O error occurs
050   */
051  protected void readFileId() throws IOException {
052    byte[] b = new byte[HEADER.length];
053    in.readFully(b);
054
055    if (!Arrays.equals(b, HEADER)) {
056      throw new IOException(
057          "file_id_string does not match. got: " + new String(b, Charsets.ISO_8859_1));
058    }
059  }
060
061  /**
062   * Read headers we don't know how to parse yet, returning the next startcode.
063   *
064   * @return The next startcode
065   * @throws IOException If a I/O error occurs
066   */
067  protected long readReservedHeaders() throws IOException {
068    long startcode = in.readStartCode();
069
070    while (Startcode.isPossibleStartcode(startcode) && isKnownStartcode(startcode)) {
071      new Packet().read(in, startcode); // Discard unknown packet
072      startcode = in.readStartCode();
073    }
074    return startcode;
075  }
076
077  /**
078   * Demux the inputstream
079   *
080   * @throws IOException If a I/O error occurs
081   */
082  public void read() throws IOException {
083    readFileId();
084    in.resetCRC();
085
086    long startcode = in.readStartCode();
087
088    while (true) {
089      // Start parsing main and stream information
090      header = new MainHeaderPacket();
091
092      if (!Startcode.MAIN.equalsCode(startcode)) {
093        throw new IOException(String.format("expected main header found: 0x%X", startcode));
094      }
095
096      header.read(in, startcode);
097      startcode = readReservedHeaders();
098
099      streams.clear();
100      for (int i = 0; i < header.streamCount; i++) {
101        if (!Startcode.STREAM.equalsCode(startcode)) {
102          throw new IOException(String.format("expected stream header found: 0x%X", startcode));
103        }
104
105        StreamHeaderPacket streamHeader = new StreamHeaderPacket();
106        streamHeader.read(in, startcode);
107
108        Stream stream = new Stream(header, streamHeader);
109        streams.add(stream);
110        listener.stream(stream);
111
112        startcode = readReservedHeaders();
113      }
114
115      while (Startcode.INFO.equalsCode(startcode)) {
116        new Packet().read(in, startcode); // Discard for the moment
117        startcode = readReservedHeaders();
118      }
119
120      if (Startcode.INDEX.equalsCode(startcode)) {
121        new Packet().read(in, startcode); // Discard for the moment
122        startcode = in.readStartCode();
123      }
124
125      // Now main frame parsing loop
126      while (!Startcode.MAIN.equalsCode(startcode)) {
127
128        if (Startcode.SYNCPOINT.equalsCode(startcode)) {
129          new Packet().read(in, startcode); // Discard for the moment
130          startcode = in.readStartCode();
131        }
132
133        if (Startcode.isPossibleStartcode(startcode)) {
134          throw new IOException("expected framecode, found " + Startcode.toString(startcode));
135        }
136
137        Frame f = new Frame();
138        f.read(this, in, (int) startcode);
139
140        listener.frame(f);
141
142        try {
143          startcode = readReservedHeaders();
144        } catch (java.io.EOFException e) {
145          return;
146        }
147      }
148    }
149  }
150}