001package net.bramp.ffmpeg.nut;
002
003import com.google.common.base.MoreObjects;
004import java.io.EOFException;
005import java.io.IOException;
006import java.nio.charset.StandardCharsets;
007import java.util.Map;
008import java.util.TreeMap;
009import org.apache.commons.lang3.math.Fraction;
010
011/** A video or audio frame. */
012public class Frame {
013  // TODO Change this to a enum
014  static final long FLAG_KEY = 1 << 0;
015  static final long FLAG_EOR = 1 << 1;
016  static final long FLAG_CODED_PTS = 1 << 3;
017  static final long FLAG_STREAM_ID = 1 << 4;
018  static final long FLAG_SIZE_MSB = 1 << 5;
019  static final long FLAG_CHECKSUM = 1 << 6;
020  static final long FLAG_RESERVED = 1 << 7;
021  static final long FLAG_SM_DATA = 1 << 8;
022  static final long FLAG_HEADER_IDX = 1 << 10;
023  static final long FLAG_MATCH_TIME = 1 << 11;
024  static final long FLAG_CODED = 1 << 12;
025  static final long FLAG_INVALID = 1 << 13;
026
027  Stream stream;
028  long flags;
029  long pts;
030  byte[] data;
031
032  Map<String, Object> sideData;
033  Map<String, Object> metaData;
034
035  /** Reads metadata key-value pairs from the NUT data input stream. */
036  protected Map<String, Object> readMetaData(NutDataInputStream in) throws IOException {
037    Map<String, Object> data = new TreeMap<String, Object>();
038    long count = in.readVarLong();
039    for (int i = 0; i < count; i++) {
040      String name = new String(in.readVarArray(), StandardCharsets.UTF_8);
041      long type = in.readSignedVarInt();
042      Object value;
043
044      if (type == -1) {
045        value = new String(in.readVarArray(), StandardCharsets.UTF_8);
046
047      } else if (type == -2) {
048        String k = new String(in.readVarArray(), StandardCharsets.UTF_8);
049        String v = new String(in.readVarArray(), StandardCharsets.UTF_8);
050        value = k + "=" + v; // TODO Change this some how
051
052      } else if (type == -3) {
053        value = in.readSignedVarInt();
054
055      } else if (type == -4) {
056        /*
057         * t (v coded universal timestamp) tmp v id= tmp % time_base_count value= (tmp /
058         * time_base_count) * timeBase[id]
059         */
060        value = in.readVarLong(); // TODO Convert to timestamp
061
062      } else if (type < -4) {
063        long denominator = -type - 4;
064        long numerator = in.readSignedVarInt();
065        value = Fraction.getFraction((int) numerator, (int) denominator);
066
067      } else {
068        value = type;
069      }
070
071      data.put(name, value);
072    }
073    return data;
074  }
075
076  /** Reads a frame from the NUT stream using the given frame code. */
077  public void read(NutReader nut, NutDataInputStream in, int code) throws IOException {
078    if (code == 'N') {
079      throw new IOException("Illegal frame code: " + code);
080    }
081
082    FrameCode fc = nut.header.frameCodes.get(code);
083    flags = fc.flags;
084    if ((flags & FLAG_INVALID) == FLAG_INVALID) {
085      throw new IOException("Using invalid framecode: " + code);
086    }
087
088    if ((flags & FLAG_CODED) == FLAG_CODED) {
089      long coded_flags = in.readVarLong();
090      flags ^= coded_flags;
091    }
092
093    int size = fc.dataSizeLsb;
094
095    int stream_id;
096    long coded_pts;
097
098    if ((flags & FLAG_STREAM_ID) == FLAG_STREAM_ID) {
099      stream_id = in.readVarInt();
100      if (stream_id >= nut.streams.size()) {
101        throw new IOException(
102            "Illegal stream id value " + stream_id + " must be < " + nut.streams.size());
103      }
104    } else {
105      stream_id = fc.streamId;
106    }
107
108    stream = nut.streams.get(stream_id);
109
110    if ((flags & FLAG_CODED_PTS) == FLAG_CODED_PTS) {
111      coded_pts = in.readVarLong();
112      if (coded_pts < (1 << stream.header.msbPtsShift)) {
113        long mask = (1L << stream.header.msbPtsShift) - 1;
114        long delta = stream.last_pts - mask / 2;
115        pts = ((coded_pts - delta) & mask) + delta;
116      } else {
117        pts = coded_pts - (1L << stream.header.msbPtsShift);
118      }
119    } else {
120      // TODO Test this code path
121      pts = stream.last_pts + fc.ptsDelta;
122    }
123    stream.last_pts = pts;
124
125    if ((flags & FLAG_SIZE_MSB) == FLAG_SIZE_MSB) {
126      int data_size_msb = in.readVarInt();
127      size += fc.dataSizeMul * data_size_msb;
128    }
129    if ((flags & FLAG_MATCH_TIME) == FLAG_MATCH_TIME) {
130      fc.matchTimeDelta = in.readSignedVarInt();
131    }
132    int header_idx = fc.headerIdx;
133    if ((flags & FLAG_HEADER_IDX) == FLAG_HEADER_IDX) {
134      header_idx = in.readVarInt();
135      if (header_idx >= nut.header.elision.size()) {
136        throw new IOException(
137            "Illegal header index " + header_idx + " must be < " + nut.header.elision.size());
138      }
139    }
140    int frame_res = fc.reservedCount;
141    if ((flags & FLAG_RESERVED) == FLAG_RESERVED) {
142      frame_res = in.readVarInt();
143    }
144
145    for (int i = 0; i < frame_res; i++) {
146      in.readVarLong(); // Discard
147    }
148
149    if ((flags & FLAG_CHECKSUM) == FLAG_CHECKSUM) {
150      @SuppressWarnings("unused")
151      long checksum = in.readInt();
152      // TODO Test checksum
153    }
154
155    if (size > 4096) {
156      header_idx = 0;
157    }
158
159    // Now data
160    if ((flags & FLAG_SM_DATA) == FLAG_SM_DATA) {
161      // TODO Test this path.
162
163      if (nut.header.version < 4) {
164        throw new IOException("Frame SM Data not allowed in version 4 or less");
165      }
166      long pos = in.offset();
167      sideData = readMetaData(in);
168      metaData = readMetaData(in);
169      long metadataLen = (in.offset() - pos);
170      if (metadataLen > size) {
171        throw new EOFException();
172      }
173
174      size -= (int) metadataLen;
175
176    } else {
177      sideData = null;
178      metaData = null;
179    }
180
181    // TODO Use some kind of byte pool
182    data = new byte[size];
183
184    byte[] elision = nut.header.elision.get(header_idx);
185    System.arraycopy(elision, 0, data, 0, elision.length);
186    in.readFully(data, elision.length, size - elision.length);
187  }
188
189  @Override
190  public String toString() {
191    return MoreObjects.toStringHelper(this)
192        .add("id", stream.header.id)
193        .add("pts", pts)
194        .add("data", String.format("(%d bytes)", data.length))
195        .toString();
196  }
197}