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