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}