]> git.basschouten.com Git - openhab-addons.git/blob
1b5c14b883ebe9ad2610ea645b18e35fe0403e24
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.getTinyUrl(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                         }
195                         output = new StreamOutput(resp, handler.mjpegContentType);
196                     }
197                 }
198                 openStreams.addStream(output);
199                 do {
200                     try {
201                         output.sendFrame();
202                     } catch (InterruptedException | IOException e) {
203                         // Never stop streaming until IOException. Occurs when browser stops the stream.
204                         openStreams.removeStream(output);
205                         logger.debug("Now there are {} ipcamera.mjpeg streams open.", openStreams.getNumberOfStreams());
206                         if (openStreams.isEmpty()) {
207                             if (output.isSnapshotBased) {
208                                 Ffmpeg localMjpeg = handler.ffmpegMjpeg;
209                                 if (localMjpeg != null) {
210                                     localMjpeg.stopConverting();
211                                 }
212                             } else {
213                                 handler.closeChannel(handler.getTinyUrl(handler.mjpegUri));
214                             }
215                             logger.debug("All ipcamera.mjpeg streams have stopped.");
216                         }
217                         return;
218                     }
219                 } while (!openStreams.isEmpty());
220             case "/autofps.mjpeg":
221                 handler.streamingAutoFps = true;
222                 output = new StreamOutput(resp);
223                 openAutoFpsStreams.addStream(output);
224                 int counter = 0;
225                 do {
226                     try {
227                         if (handler.motionDetected) {
228                             output.sendSnapshotBasedFrame(handler.getSnapshot());
229                         } // every 8 seconds if no motion or the first three snapshots to fill any FIFO
230                         else if (counter % 8 == 0 || counter < 3) {
231                             output.sendSnapshotBasedFrame(handler.getSnapshot());
232                         }
233                         counter++;
234                         Thread.sleep(1000);
235                     } catch (InterruptedException | IOException e) {
236                         // Never stop streaming until IOException. Occurs when browser stops the stream.
237                         openAutoFpsStreams.removeStream(output);
238                         logger.debug("Now there are {} autofps.mjpeg streams open.",
239                                 openAutoFpsStreams.getNumberOfStreams());
240                         if (openAutoFpsStreams.isEmpty()) {
241                             handler.streamingAutoFps = false;
242                             logger.debug("All autofps.mjpeg streams have stopped.");
243                         }
244                         return;
245                     }
246                 } while (true);
247             case "/instar":
248                 InstarHandler instar = new InstarHandler(handler);
249                 instar.alarmTriggered(pathInfo + "?" + req.getQueryString());
250                 return;
251             default:
252                 if (pathInfo.endsWith(".ts")) {
253                     sendFile(resp, pathInfo, "video/MP2T");
254                 } else if (pathInfo.endsWith(".gif")) {
255                     sendFile(resp, pathInfo, "image/gif");
256                 } else if (pathInfo.endsWith(".jpg")) {
257                     // Allow access to the preroll and postroll jpg files
258                     sendFile(resp, pathInfo, "image/jpg");
259                 } else if (pathInfo.endsWith(".mp4")) {
260                     sendFile(resp, pathInfo, "video/mp4");
261                 }
262                 return;
263         }
264     }
265
266     @Override
267     protected void sendFile(HttpServletResponse response, String filename, String contentType) throws IOException {
268         // Ensure no files can be sourced from parent or child folders
269         String truncated = filename.substring(filename.lastIndexOf("/"));
270         super.sendFile(response, handler.cameraConfig.getFfmpegOutput() + truncated, contentType);
271     }
272
273     @Override
274     public void dispose() {
275         openStreams.closeAllStreams();
276         openSnapshotStreams.closeAllStreams();
277         openAutoFpsStreams.closeAllStreams();
278         super.dispose();
279     }
280 }