]> git.basschouten.com Git - openhab-addons.git/blob
dfe9af68dbc41d0e117a6769b693476a78ae4579
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.ipcamera.internal.servlet;
14
15 import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.HLS_STARTUP_DELAY_MS;
16
17 import java.io.IOException;
18 import java.time.Duration;
19 import java.time.Instant;
20
21 import javax.servlet.AsyncContext;
22 import javax.servlet.ServletInputStream;
23 import javax.servlet.http.HttpServletRequest;
24 import javax.servlet.http.HttpServletResponse;
25
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.ipcamera.internal.ChannelTracking;
29 import org.openhab.binding.ipcamera.internal.Ffmpeg;
30 import org.openhab.binding.ipcamera.internal.InstarHandler;
31 import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
32 import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
33 import org.osgi.service.http.HttpService;
34
35 /**
36  * The {@link CameraServlet} is responsible for serving files for a single camera back to the Jetty server normally
37  * found on port 8080
38  *
39  * @author Matthew Skinner - Initial contribution
40  */
41 @NonNullByDefault
42 public class CameraServlet extends IpCameraServlet {
43     private static final long serialVersionUID = -134658667574L;
44     private final IpCameraHandler handler;
45     public OpenStreams openStreams = new OpenStreams();
46     private OpenStreams openSnapshotStreams = new OpenStreams();
47     private OpenStreams openAutoFpsStreams = new OpenStreams();
48
49     public CameraServlet(IpCameraHandler handler, HttpService httpService) {
50         super(handler, httpService);
51         this.handler = handler;
52     }
53
54     @Override
55     protected void doPost(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp) throws IOException {
56         if (req == null || resp == null) {
57             return;
58         }
59         String pathInfo = req.getPathInfo();
60         if (pathInfo == null) {
61             return;
62         }
63         switch (pathInfo) {
64             case "/ipcamera.jpg":
65                 // ffmpeg sends data here for ipcamera.mjpeg streams when camera has no native stream.
66                 ServletInputStream snapshotData = req.getInputStream();
67                 openStreams.queueFrame(snapshotData.readAllBytes());
68                 snapshotData.close();
69                 break;
70             case "/snapshot.jpg":
71                 snapshotData = req.getInputStream();
72                 handler.processSnapshot(snapshotData.readAllBytes());
73                 snapshotData.close();
74                 break;
75             case "/OnvifEvent":
76                 handler.onvifCamera.eventRecieved(req.getReader().toString());
77                 break;
78             default:
79                 logger.debug("Recieved unknown request \tPOST:{}", pathInfo);
80                 break;
81         }
82     }
83
84     @Override
85     protected void doGet(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp) throws IOException {
86         if (req == null || resp == null) {
87             return;
88         }
89         String pathInfo = req.getPathInfo();
90         if (pathInfo == null) {
91             return;
92         }
93         logger.debug("GET:{}, received from {}", pathInfo, req.getRemoteHost());
94         if (!"DISABLE".equals(handler.getWhiteList())) {
95             String requestIP = "(" + req.getRemoteHost() + ")";
96             if (!handler.getWhiteList().contains(requestIP)) {
97                 logger.warn("The request made from {} was not in the whiteList and will be ignored.", requestIP);
98                 return;
99             }
100         }
101         switch (pathInfo) {
102             case "/ipcamera.m3u8":
103                 Ffmpeg localFfmpeg = handler.ffmpegHLS;
104                 if (localFfmpeg == null) {
105                     handler.setupFfmpegFormat(FFmpegFormat.HLS);
106                 } else if (!localFfmpeg.getIsAlive()) {
107                     localFfmpeg.startConverting();
108                 } else {
109                     localFfmpeg.setKeepAlive(8);
110                     sendFile(resp, pathInfo, "application/x-mpegURL");
111                     return;
112                 }
113                 // Allow files to be created, or you get old m3u8 from the last time this ran.
114                 try {
115                     Thread.sleep(HLS_STARTUP_DELAY_MS);
116                 } catch (InterruptedException e) {
117                     return;
118                 }
119                 sendFile(resp, pathInfo, "application/x-mpegURL");
120                 return;
121             case "/ipcamera.mpd":
122                 sendFile(resp, pathInfo, "application/dash+xml");
123                 return;
124             case "/ipcamera.gif":
125                 sendFile(resp, pathInfo, "image/gif");
126                 return;
127             case "/ipcamera.jpg":
128                 // Use cached image if recent. Cameras can take > 1sec to send back a reply.
129                 // Example an Image item/widget may have a 1 second refresh.
130                 if (handler.ffmpegSnapshotGeneration
131                         || Duration.between(handler.currentSnapshotTime, Instant.now()).toMillis() < 1200) {
132                     sendSnapshotImage(resp, "image/jpg", handler.getSnapshot());
133                 } else {
134                     handler.getSnapshot();
135                     final AsyncContext acontext = req.startAsync(req, resp);
136                     acontext.start(new Runnable() {
137                         @Override
138                         public void run() {
139                             Instant startTime = Instant.now();
140                             do {
141                                 try {
142                                     Thread.sleep(100);
143                                 } catch (InterruptedException e) {
144                                     return;
145                                 }
146                             } // 5 sec timeout OR a new snapshot comes back from camera
147                             while (Duration.between(startTime, Instant.now()).toMillis() < 5000
148                                     && Duration.between(handler.currentSnapshotTime, Instant.now()).toMillis() > 1200);
149                             sendSnapshotImage(resp, "image/jpg", handler.getSnapshot());
150                             acontext.complete();
151                         }
152                     });
153                 }
154                 return;
155             case "/snapshots.mjpeg":
156                 handler.streamingSnapshotMjpeg = true;
157                 handler.startSnapshotPolling();
158                 StreamOutput output = new StreamOutput(resp);
159                 openSnapshotStreams.addStream(output);
160                 do {
161                     try {
162                         output.sendSnapshotBasedFrame(handler.getSnapshot());
163                         Thread.sleep(handler.cameraConfig.getPollTime());
164                     } catch (InterruptedException | IOException e) {
165                         // Never stop streaming until IOException. Occurs when browser stops the stream.
166                         openSnapshotStreams.removeStream(output);
167                         logger.debug("Now there are {} snapshots.mjpeg streams open.",
168                                 openSnapshotStreams.getNumberOfStreams());
169                         if (openSnapshotStreams.isEmpty()) {
170                             handler.streamingSnapshotMjpeg = false;
171                             handler.stopSnapshotPolling();
172                             logger.debug("All snapshots.mjpeg streams have stopped.");
173                         }
174                         return;
175                     }
176                 } while (true);
177             case "/ipcamera.mjpeg":
178                 if (openStreams.isEmpty()) {
179                     logger.debug("First stream requested, opening up stream from camera");
180                     handler.openCamerasStream();
181                     if (handler.mjpegUri.isEmpty() || "ffmpeg".equals(handler.mjpegUri)) {
182                         output = new StreamOutput(resp);
183                     } else {
184                         output = new StreamOutput(resp, handler.mjpegContentType);
185                     }
186                 } else {
187                     if (handler.mjpegUri.isEmpty() || "ffmpeg".equals(handler.mjpegUri)) {
188                         output = new StreamOutput(resp);
189                     } else {
190                         ChannelTracking tracker = handler.channelTrackingMap.get(handler.mjpegUri);
191                         if (tracker == null || !tracker.getChannel().isOpen()) {
192                             logger.debug("Not the first stream requested but the stream from camera was closed");
193                             handler.openCamerasStream();
194                             openStreams.closeAllStreams();
195                         }
196                         output = new StreamOutput(resp, handler.mjpegContentType);
197                     }
198                 }
199                 openStreams.addStream(output);
200                 do {
201                     try {
202                         output.sendFrame();
203                     } catch (InterruptedException | IOException e) {
204                         // Never stop streaming until IOException. Occurs when browser stops the stream.
205                         openStreams.removeStream(output);
206                         logger.debug("Now there are {} ipcamera.mjpeg streams open.", openStreams.getNumberOfStreams());
207                         if (openStreams.isEmpty()) {
208                             if (output.isSnapshotBased) {
209                                 Ffmpeg localMjpeg = handler.ffmpegMjpeg;
210                                 if (localMjpeg != null) {
211                                     localMjpeg.stopConverting();
212                                 }
213                             } else {
214                                 handler.closeChannel(handler.getTinyUrl(handler.mjpegUri));
215                             }
216                             logger.debug("All ipcamera.mjpeg streams have stopped.");
217                         }
218                         return;
219                     }
220                 } while (!openStreams.isEmpty());
221             case "/autofps.mjpeg":
222                 handler.streamingAutoFps = true;
223                 output = new StreamOutput(resp);
224                 openAutoFpsStreams.addStream(output);
225                 int counter = 0;
226                 do {
227                     try {
228                         if (handler.motionDetected) {
229                             output.sendSnapshotBasedFrame(handler.getSnapshot());
230                         } // every 8 seconds if no motion or the first three snapshots to fill any FIFO
231                         else if (counter % 8 == 0 || counter < 3) {
232                             output.sendSnapshotBasedFrame(handler.getSnapshot());
233                         }
234                         counter++;
235                         Thread.sleep(1000);
236                     } catch (InterruptedException | IOException e) {
237                         // Never stop streaming until IOException. Occurs when browser stops the stream.
238                         openAutoFpsStreams.removeStream(output);
239                         logger.debug("Now there are {} autofps.mjpeg streams open.",
240                                 openAutoFpsStreams.getNumberOfStreams());
241                         if (openAutoFpsStreams.isEmpty()) {
242                             handler.streamingAutoFps = false;
243                             logger.debug("All autofps.mjpeg streams have stopped.");
244                         }
245                         return;
246                     }
247                 } while (true);
248             case "/instar":
249                 InstarHandler instar = new InstarHandler(handler);
250                 instar.alarmTriggered(pathInfo + "?" + req.getQueryString());
251                 return;
252             default:
253                 if (pathInfo.endsWith(".ts")) {
254                     sendFile(resp, pathInfo, "video/MP2T");
255                 } else if (pathInfo.endsWith(".gif")) {
256                     sendFile(resp, pathInfo, "image/gif");
257                 } else if (pathInfo.endsWith(".jpg")) {
258                     // Allow access to the preroll and postroll jpg files
259                     sendFile(resp, pathInfo, "image/jpg");
260                 } else if (pathInfo.endsWith(".mp4")) {
261                     sendFile(resp, pathInfo, "video/mp4");
262                 }
263                 return;
264         }
265     }
266
267     @Override
268     protected void sendFile(HttpServletResponse response, String filename, String contentType) throws IOException {
269         // Ensure no files can be sourced from parent or child folders
270         String truncated = filename.substring(filename.lastIndexOf("/"));
271         super.sendFile(response, handler.cameraConfig.getFfmpegOutput() + truncated, contentType);
272     }
273
274     @Override
275     public void dispose() {
276         openStreams.closeAllStreams();
277         openSnapshotStreams.closeAllStreams();
278         openAutoFpsStreams.closeAllStreams();
279         super.dispose();
280     }
281 }