Commit | Line | Data |
---|---|---|
e59e98fc TW |
1 | package kaka.cakelight; |
2 | ||
eba8feca TW |
3 | import javax.imageio.ImageIO; |
4 | import java.awt.image.BufferedImage; | |
e59e98fc | 5 | import java.io.*; |
4a2d6056 | 6 | import java.util.Optional; |
e59e98fc | 7 | |
4a2d6056 TW |
8 | import static kaka.cakelight.Main.log; |
9 | ||
10 | public 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 | } |