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