Fix for Theora streams with pixel aspect ratio of zero or infinity.
[cortado.git] / src / com / fluendo / examples / DumpVideo.java
1 /* Copyright (C) <2009> Maik Merten <maikmerten@googlemail.com>
2  * Copyright (C) <2004> Wim Taymans <wim@fluendo.com> (TheoraDec.java parts)
3  *
4  * This library is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU Library General Public
6  * License as published by the Free Software Foundation; either
7  * version 2 of the License, or (at your option) any later version.
8  *
9  * This library is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  * Library General Public License for more details.
13  *
14  * You should have received a copy of the GNU Library General Public
15  * License along with this library; if not, write to the
16  * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
17  * Boston, MA 02111-1307, USA.
18  */
19 package com.fluendo.examples;
20
21 import com.fluendo.jheora.Comment;
22 import com.fluendo.jheora.Info;
23 import com.fluendo.jheora.State;
24 import com.fluendo.jheora.YUVBuffer;
25 import com.fluendo.utils.Debug;
26 import com.fluendo.utils.MemUtils;
27 import com.jcraft.jogg.Packet;
28 import com.jcraft.jogg.Page;
29 import com.jcraft.jogg.StreamState;
30 import com.jcraft.jogg.SyncState;
31 import java.io.File;
32 import java.io.FileInputStream;
33 import java.io.FileNotFoundException;
34 import java.io.FileOutputStream;
35 import java.io.IOException;
36 import java.io.InputStream;
37 import java.io.OutputStream;
38 import java.util.Date;
39 import java.util.HashMap;
40 import java.util.HashSet;
41 import java.util.LinkedList;
42 import java.util.List;
43 import java.util.Map;
44 import java.util.Set;
45
46
47 /**
48  * This class borrows code from TheoraDec.java
49  */
50 public class DumpVideo {
51
52     public static final Integer OK = new Integer(0);
53     public static final Integer ERROR = new Integer(-5);
54     private static final byte[] signature = {-128, 0x74, 0x68, 0x65, 0x6f, 0x72, 0x61};
55
56     private class TheoraDecoderWrapper {
57
58         private Info ti;
59         private Comment tc;
60         private State ts;
61         private YUVBuffer yuv;
62         private int packet;
63         private int frame;
64         private boolean needKeyframe;
65
66         public TheoraDecoderWrapper() {
67             super();
68             ti = new Info();
69             tc = new Comment();
70             ts = new State();
71             yuv = new YUVBuffer();
72         }
73
74         public int takeHeader(Packet op) {
75             int ret;
76             byte header;
77             ret = ti.decodeHeader(tc, op);
78             header = op.packet_base[op.packet];
79             if (header == -126) {
80                 ts.decodeInit(ti);
81             }
82             return ret;
83         }
84
85         public boolean isHeader(Packet op) {
86             return (op.packet_base[op.packet] & 0x80) == 0x80;
87         }
88
89         public boolean isKeyFrame(Packet op) {
90             return ts.isKeyframe(op);
91         }
92
93         public Object decode(Packet op) {
94
95             Object result = OK;
96
97
98             if (packet < 3) {
99                 //System.out.println ("decoding header");
100                 if (takeHeader(op) < 0) {
101                     // error case; not a theora header
102                     Debug.log(Debug.ERROR, "does not contain Theora video data.");
103                     return ERROR;
104                 }
105                 if (packet == 2) {
106                     ts.decodeInit(ti);
107
108                     Debug.log(Debug.INFO, "theora dimension: " + ti.width + "x" + ti.height);
109                     if (ti.aspect_denominator == 0) {
110                         ti.aspect_numerator = 1;
111                         ti.aspect_denominator = 1;
112                     }
113                     Debug.log(Debug.INFO, "theora offset: " + ti.offset_x + "," + ti.offset_y);
114                     Debug.log(Debug.INFO, "theora frame: " + ti.frame_width + "," + ti.frame_height);
115                     Debug.log(Debug.INFO, "theora aspect: " + ti.aspect_numerator + "/" + ti.aspect_denominator);
116                     Debug.log(Debug.INFO, "theora framerate: " + ti.fps_numerator + "/" + ti.fps_denominator);
117
118                 }
119                 packet++;
120
121                 return OK;
122             } else {
123                 if ((op.packet_base[op.packet] & 0x80) == 0x80) {
124                     Debug.log(Debug.INFO, "ignoring header");
125                     return OK;
126                 }
127                 if (needKeyframe && ts.isKeyframe(op)) {
128                     needKeyframe = false;
129                 }
130
131
132                 if (!needKeyframe) {
133                     try {
134                         if (ts.decodePacketin(op) != 0) {
135                             Debug.log(Debug.ERROR, "Bad Theora packet. Most likely not fatal, hoping for better luck next packet.");
136                         }
137                         if (ts.decodeYUVout(yuv) != 0) {
138                             Debug.log(Debug.ERROR, "Error getting the picture.");
139                             return ERROR;
140                         }
141
142                         System.out.println("Decoded frame: " + ++frame);
143
144                         return yuv.getObject(ti.offset_x, ti.offset_y, ti.frame_width, ti.frame_height);
145                     } catch (Exception e) {
146                         e.printStackTrace();
147                         result = ERROR;
148                     }
149                 } else {
150                     result = OK;
151                 }
152             }
153             packet++;
154
155             return result;
156         }
157     }
158
159     private class YUVWriter {
160
161         private OutputStream os;
162         private boolean wroteHeader = false;
163         private byte[] ybytes;
164         private byte[] uvbytes;
165         private boolean raw;
166
167         public YUVWriter(File outfile, boolean raw) {
168             this.raw = raw;
169             try {
170                 os = new FileOutputStream(outfile);
171             } catch (FileNotFoundException ex) {
172                 ex.printStackTrace();
173             }
174         }
175
176         public void writeYUVFrame(Info ti, YUVBuffer yuv) {
177             try {
178                 if (!raw) {
179                     if (!wroteHeader) {
180                         String headerstring = "YUV4MPEG2 W" + ti.width + " H" + ti.height + " F" + ti.fps_numerator + ":" + ti.fps_denominator + " Ip A" + ti.aspect_numerator + ":" + ti.aspect_denominator + "\n";
181                         os.write(headerstring.getBytes());
182                         wroteHeader = true;
183                     }
184                     os.write("FRAME\n".getBytes());
185                 }
186
187                 if (ybytes == null || ybytes.length != yuv.y_width * yuv.y_height) {
188                     ybytes = new byte[yuv.y_width * yuv.y_height];
189                 }
190
191                 int offset = 0;
192                 for (int i = 0; i < yuv.y_height; ++i) {
193                     int start = yuv.y_offset + (i * yuv.y_stride);
194                     for (int j = start; j < start + yuv.y_width; ++j) {
195                         ybytes[offset++] = (byte) yuv.data[j];
196                     }
197
198                 }
199                 os.write(ybytes);
200
201                 if (uvbytes == null || uvbytes.length != yuv.uv_width * yuv.uv_height) {
202                     uvbytes = new byte[yuv.uv_width * yuv.uv_height];
203                 }
204
205                 offset = 0;
206                 for (int i = 0; i < yuv.uv_height; ++i) {
207                     int start = yuv.u_offset + (i * yuv.uv_stride);
208                     for (int j = start; j < start + yuv.uv_width; ++j) {
209                         uvbytes[offset++] = (byte) yuv.data[j];
210                     }
211                 }
212                 os.write(uvbytes);
213
214                 offset = 0;
215                 for (int i = 0; i < yuv.uv_height; ++i) {
216                     int start = yuv.v_offset + (i * yuv.uv_stride);
217                     for (int j = start; j < start + yuv.uv_width; ++j) {
218                         uvbytes[offset++] = (byte) yuv.data[j];
219                     }
220                 }
221                 os.write(uvbytes);
222
223             } catch (IOException ex) {
224                 ex.printStackTrace();
225             }
226         }
227     }
228
229     public boolean isTheora(Packet op) {
230         return typeFind(op.packet_base, op.packet, op.bytes) > 0;
231     }
232
233     public int typeFind(byte[] data, int offset, int length) {
234         if (MemUtils.startsWith(data, offset, length, signature)) {
235             return 10;
236         }
237         return -1;
238     }
239
240     public void dumpVideo(File videofile, List outfiles, boolean raw) throws IOException {
241         InputStream is = new FileInputStream(videofile);
242
243         boolean onlytime = outfiles.size() == 0;
244
245         SyncState oy = new SyncState();
246         Page og = new Page();
247         Packet op = new Packet();
248         byte[] buf = new byte[512];
249
250         Map streamstates = new HashMap();
251         Map theoradecoders = new HashMap();
252         Map yuvwriters = new HashMap();
253         Set hasdecoder = new HashSet();
254
255         int read = is.read(buf);
256         while (read > 0) {
257             int offset = oy.buffer(read);
258             java.lang.System.arraycopy(buf, 0, oy.data, offset, read);
259             oy.wrote(read);
260
261             while (oy.pageout(og) == 1) {
262
263                 Integer serialno = new Integer(og.serialno());
264
265                 StreamState state = (StreamState) streamstates.get(serialno);
266                 if (state == null) {
267                     state = new StreamState();
268                     state.init(serialno.intValue());
269                     streamstates.put(serialno, state);
270                     Debug.info("created StreamState for stream no. " + og.serialno());
271                 }
272
273                 state.pagein(og);
274
275                 while (state.packetout(op) == 1) {
276
277                     if (!(hasdecoder.contains(serialno)) && isTheora(op)) {
278
279                         TheoraDecoderWrapper theoradec = (TheoraDecoderWrapper) theoradecoders.get(serialno);
280                         if (theoradec == null) {
281                             theoradec = new TheoraDecoderWrapper();
282                             theoradecoders.put(serialno, theoradec);
283                             hasdecoder.add(serialno);
284                         }
285
286                         Debug.info("is Theora: " + serialno);
287                     }
288
289                     TheoraDecoderWrapper theoradec = (TheoraDecoderWrapper) theoradecoders.get(serialno);
290
291                     if (theoradec != null) {
292                         Object result = theoradec.decode(op);
293                         if (!onlytime && result instanceof YUVBuffer) {
294
295                             YUVWriter yuvwriter = (YUVWriter) yuvwriters.get(serialno);
296                             if (yuvwriter == null && !outfiles.isEmpty()) {
297                                 yuvwriter = new YUVWriter((File) outfiles.get(0), raw);
298                                 yuvwriters.put(serialno, yuvwriter);
299                                 outfiles.remove(0);
300                             }
301
302                             if (yuvwriter != null) {
303                                 YUVBuffer yuvbuf = (YUVBuffer) result;
304                                 yuvwriter.writeYUVFrame(theoradec.ti, yuvbuf);
305                             }
306
307                         }
308                     }
309                 }
310             }
311
312             read = is.read(buf);
313         }
314
315     }
316
317     public static void main(String[] args) throws IOException {
318
319         if (args.length < 1) {
320             System.err.println("usage: DumpVideo <videofile> [<outfile_1> ... <outfile_n>} [--raw>]");
321             System.exit(1);
322         }
323
324         boolean raw = false;
325         File infile = new File(args[0]);
326
327         List outfiles = new LinkedList();
328         for (int i = 1; i < args.length; ++i) {
329             if(args[i].equals("--raw")) {
330                 raw = true;
331                 break;
332             }
333             outfiles.add(new File(args[i]));
334         }
335
336         if(outfiles.size() == 0) {
337             System.out.println("no output files given, will only time decode");
338         }
339
340         DumpVideo dv = new DumpVideo();
341
342         Date start = new Date();
343         dv.dumpVideo(infile, outfiles, raw);
344         Date end = new Date();
345
346         System.out.println("time: " + (end.getTime() - start.getTime()) + " milliseconds");
347
348     }
349 }