Add very special handling for the MJPG video format
[kaka/cakelight.git] / src / kaka / cakelight / FrameGrabber.java
1 package kaka.cakelight;
2
3 import javax.imageio.ImageIO;
4 import java.awt.image.BufferedImage;
5 import java.io.*;
6 import java.util.Optional;
7
8 import static kaka.cakelight.Main.log;
9
10 public class FrameGrabber implements Closeable {
11     private Configuration config;
12     private File file;
13     private int bytesPerFrame;
14     private InputStream fileStream;
15     private final ByteArrayOutputStream bufferedBytes = new ByteArrayOutputStream();
16
17     private FrameGrabber() {
18     }
19
20     public static FrameGrabber from(File videoDevice, Configuration config) {
21         FrameGrabber fg = new FrameGrabber();
22         fg.config = config;
23         fg.file = videoDevice;
24         fg.bytesPerFrame = config.video.width * config.video.height * config.video.bpp;
25         fg.prepare();
26         return fg;
27     }
28
29     private boolean prepare() {
30         try {
31             fileStream = new FileInputStream(file);
32             return true;
33         } catch (FileNotFoundException e) {
34             // TODO: handle java.io.FileNotFoundException: /dev/video1 (Permission denied)
35             e.printStackTrace();
36             return false;
37         }
38     }
39
40     /**
41      * Must be run in the same thread as {@link #prepare}.
42      */
43     public Optional<VideoFrame> grabFrame() {
44         try {
45             byte[] data;
46             if (config.video.mjpg) {
47                 byte[] jpgData = readStreamingJpgData();
48                 if (jpgData == null) {
49                     return Optional.empty();
50                 }
51                 saveTemporaryJpgFile(jpgData);
52                 byte[] bmpData = convertJpgFileToByteArray();
53                 if (bmpData == null) {
54                     return Optional.empty();
55                 }
56                 data = bmpData;
57             } else {
58                 data = new byte[bytesPerFrame];
59                 int count = fileStream.read(data);
60                 if (count != bytesPerFrame) {
61                     log("Expected to read " + bytesPerFrame + " bytes per frame but read " + count);
62                 }
63             }
64
65             return Optional.of(VideoFrame.of(data, config));
66         } catch (IOException e) {
67             e.printStackTrace();
68         }
69
70         return Optional.empty();
71     }
72
73     private byte[] readStreamingJpgData() throws IOException {
74         byte[] data;
75         byte[] batch = new byte[1024];
76         boolean lastByteIsXX = false;
77         loop:
78         while (true) {
79             int batchCount = fileStream.read(batch);
80             if (batchCount == -1) {
81                 return null;
82             }
83             if (lastByteIsXX) {
84                 if (batch[0] == (byte) 0xd8) {
85                     data = bufferedBytes.toByteArray();
86                     bufferedBytes.reset();
87                     bufferedBytes.write(0xff);
88                     bufferedBytes.write(batch, 0, batchCount);
89                     break;
90                 }
91                 bufferedBytes.write(0xff);
92             }
93             for (int i = 0; i < batchCount - 1; i++) {
94                 if (batch[i] == (byte) 0xff && batch[i + 1] == (byte) 0xd8) { // start of jpeg
95                     if (i > 0) {
96                         bufferedBytes.write(batch, 0, i);
97                     }
98                     data = bufferedBytes.toByteArray();
99                     bufferedBytes.reset();
100                     bufferedBytes.write(batch, i, batchCount - i);
101                     break loop;
102                 }
103             }
104             lastByteIsXX = batch[batchCount - 1] == (byte) 0xff;
105             bufferedBytes.write(batch, 0, batchCount - (lastByteIsXX ? 1 : 0));
106         }
107         return data;
108     }
109
110     private void saveTemporaryJpgFile(byte[] data) throws IOException {
111         try (FileOutputStream fos = new FileOutputStream("/tmp/cakelight-video-stream.jpg")) {
112             fos.write(data);
113         }
114     }
115
116     private byte[] convertJpgFileToByteArray() throws IOException {
117         BufferedImage image = ImageIO.read(new File("/tmp/cakelight-video-stream.jpg"));
118         if (image != null) { // will almost always be null the first time
119             try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
120                 ImageIO.write(image, "bmp", baos);
121                 baos.flush();
122                 return baos.toByteArray();
123             }
124         } else {
125             return null;
126         }
127     }
128
129     @Override
130     public void close() throws IOException {
131         fileStream.close();
132     }
133 }