Add very special handling for the MJPG video format
[kaka/cakelight.git] / src / kaka / cakelight / FrameGrabber.java
CommitLineData
e59e98fc
TW
1package kaka.cakelight;
2
eba8feca
TW
3import javax.imageio.ImageIO;
4import java.awt.image.BufferedImage;
e59e98fc 5import java.io.*;
4a2d6056 6import java.util.Optional;
e59e98fc 7
4a2d6056
TW
8import static kaka.cakelight.Main.log;
9
10public class FrameGrabber implements Closeable {
e59e98fc
TW
11 private Configuration config;
12 private File file;
13 private int bytesPerFrame;
14 private InputStream fileStream;
eba8feca 15 private final ByteArrayOutputStream bufferedBytes = new ByteArrayOutputStream();
e59e98fc
TW
16
17 private FrameGrabber() {
18 }
19
03670958 20 public static FrameGrabber from(File videoDevice, Configuration config) {
e59e98fc
TW
21 FrameGrabber fg = new FrameGrabber();
22 fg.config = config;
03670958 23 fg.file = videoDevice;
e59e98fc 24 fg.bytesPerFrame = config.video.width * config.video.height * config.video.bpp;
4a2d6056 25 fg.prepare();
e59e98fc
TW
26 return fg;
27 }
28
4a2d6056 29 private boolean prepare() {
e59e98fc
TW
30 try {
31 fileStream = new FileInputStream(file);
32 return true;
33 } catch (FileNotFoundException e) {
b72a3fc5 34 // TODO: handle java.io.FileNotFoundException: /dev/video1 (Permission denied)
e59e98fc
TW
35 e.printStackTrace();
36 return false;
37 }
38 }
39
4a2d6056
TW
40 /**
41 * Must be run in the same thread as {@link #prepare}.
42 */
adc29b9a 43 public Optional<VideoFrame> grabFrame() {
e59e98fc 44 try {
eba8feca
TW
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 }
48d4699c 63 }
eba8feca 64
adc29b9a 65 return Optional.of(VideoFrame.of(data, config));
e59e98fc
TW
66 } catch (IOException e) {
67 e.printStackTrace();
68 }
69
4a2d6056 70 return Optional.empty();
e59e98fc
TW
71 }
72
eba8feca
TW
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
4a2d6056
TW
129 @Override
130 public void close() throws IOException {
131 fileStream.close();
e59e98fc
TW
132 }
133}