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}