001package net.bramp.ffmpeg.nut;
002
003import static com.google.common.base.Preconditions.checkNotNull;
004import static net.bramp.ffmpeg.nut.Packet.Startcode;
005
006import java.io.IOException;
007import java.io.InputStream;
008import java.nio.charset.StandardCharsets;
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  /** Constructs a new NutReader for the given input stream and listener. */
038  public NutReader(InputStream in, NutReaderListener listener) {
039    this.in = new NutDataInputStream(in);
040    this.listener = checkNotNull(listener);
041  }
042
043  /** Returns whether the given startcode is a known NUT packet startcode. */
044  public static boolean isKnownStartcode(long startcode) {
045    return Startcode.of(startcode) != null;
046  }
047
048  /**
049   * Read the magic at the beginning of the file.
050   *
051   * @throws IOException If a I/O error occurs
052   */
053  protected void readFileId() throws IOException {
054    byte[] b = new byte[HEADER.length];
055    in.readFully(b);
056
057    if (!Arrays.equals(b, HEADER)) {
058      throw new IOException(
059          "file_id_string does not match. got: " + new String(b, StandardCharsets.ISO_8859_1));
060    }
061  }
062
063  /**
064   * Read headers we don't know how to parse yet, returning the next startcode.
065   *
066   * @return The next startcode
067   * @throws IOException If a I/O error occurs
068   */
069  protected long readReservedHeaders() throws IOException {
070    long startcode = in.readStartCode();
071
072    while (Startcode.isPossibleStartcode(startcode) && isKnownStartcode(startcode)) {
073      new Packet().read(in, startcode); // Discard unknown packet
074      startcode = in.readStartCode();
075    }
076    return startcode;
077  }
078
079  /**
080   * Demux the inputstream.
081   *
082   * @throws IOException If a I/O error occurs
083   */
084  public void read() throws IOException {
085    readFileId();
086    in.resetCRC();
087
088    long startcode = in.readStartCode();
089
090    while (true) {
091      // Start parsing main and stream information
092      header = new MainHeaderPacket();
093
094      if (!Startcode.MAIN.equalsCode(startcode)) {
095        throw new IOException(String.format("expected main header found: 0x%X", startcode));
096      }
097
098      header.read(in, startcode);
099      startcode = readReservedHeaders();
100
101      streams.clear();
102      for (int i = 0; i < header.streamCount; i++) {
103        if (!Startcode.STREAM.equalsCode(startcode)) {
104          throw new IOException(String.format("expected stream header found: 0x%X", startcode));
105        }
106
107        StreamHeaderPacket streamHeader = new StreamHeaderPacket();
108        streamHeader.read(in, startcode);
109
110        Stream stream = new Stream(header, streamHeader);
111        streams.add(stream);
112        listener.stream(stream);
113
114        startcode = readReservedHeaders();
115      }
116
117      while (Startcode.INFO.equalsCode(startcode)) {
118        new Packet().read(in, startcode); // Discard for the moment
119        startcode = readReservedHeaders();
120      }
121
122      if (Startcode.INDEX.equalsCode(startcode)) {
123        new Packet().read(in, startcode); // Discard for the moment
124        startcode = in.readStartCode();
125      }
126
127      // Now main frame parsing loop
128      while (!Startcode.MAIN.equalsCode(startcode)) {
129
130        if (Startcode.SYNCPOINT.equalsCode(startcode)) {
131          new Packet().read(in, startcode); // Discard for the moment
132          startcode = in.readStartCode();
133        }
134
135        if (Startcode.isPossibleStartcode(startcode)) {
136          throw new IOException("expected framecode, found " + Startcode.toString(startcode));
137        }
138
139        Frame f = new Frame();
140        f.read(this, in, (int) startcode);
141
142        listener.frame(f);
143
144        try {
145          startcode = readReservedHeaders();
146        } catch (java.io.EOFException e) {
147          return;
148        }
149      }
150    }
151  }
152}