]> git.basschouten.com Git - openhab-addons.git/blob
d8f439151c553e1eed824720e10bc2290aca267e
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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(1005);
164                     } catch (InterruptedException | IOException e) {
165                         // Never stop streaming until IOException. Occurs when browser stops the stream.
166                         openSnapshotStreams.removeStream(output);
167                         if (openSnapshotStreams.isEmpty()) {
168                             handler.streamingSnapshotMjpeg = false;
169                             handler.stopSnapshotPolling();
170                             logger.debug("All snapshots.mjpeg streams have stopped.");
171                         }
172                         return;
173                     }
174                 } while (true);
175             case "/ipcamera.mjpeg":
176                 if (handler.mjpegUri.isEmpty() || "ffmpeg".equals(handler.mjpegUri)) {
177                     if (openStreams.isEmpty()) {
178                         handler.setupFfmpegFormat(FFmpegFormat.MJPEG);
179                     }
180                     output = new StreamOutput(resp);
181                     openStreams.addStream(output);
182                 } else if (openStreams.isEmpty()) {
183                     logger.debug("First stream requested, opening up stream from camera");
184                     handler.openCamerasStream();
185                     output = new StreamOutput(resp, handler.mjpegContentType);
186                     openStreams.addStream(output);
187                 } else {
188                     ChannelTracking tracker = handler.channelTrackingMap.get(handler.mjpegUri);
189                     if (tracker == null || !tracker.getChannel().isOpen()) {
190                         logger.warn("Not the first stream requested but the stream from camera was closed");
191                         handler.openCamerasStream();
192                     }
193                     output = new StreamOutput(resp, handler.mjpegContentType);
194                     openStreams.addStream(output);
195                 }
196                 do {
197                     try {
198                         output.sendFrame();
199                     } catch (InterruptedException | IOException e) {
200                         // Never stop streaming until IOException. Occurs when browser stops the stream.
201                         openStreams.removeStream(output);
202                         if (openStreams.isEmpty()) {
203                             if (output.isSnapshotBased) {
204                                 Ffmpeg localMjpeg = handler.ffmpegMjpeg;
205                                 if (localMjpeg != null) {
206                                     localMjpeg.stopConverting();
207                                 }
208                             } else {
209                                 handler.closeChannel(handler.getTinyUrl(handler.mjpegUri));
210                             }
211                             logger.debug("All ipcamera.mjpeg streams have stopped.");
212                         }
213                         return;
214                     }
215                 } while (!openStreams.isEmpty());
216             case "/autofps.mjpeg":
217                 handler.streamingAutoFps = true;
218                 output = new StreamOutput(resp);
219                 openAutoFpsStreams.addStream(output);
220                 int counter = 0;
221                 do {
222                     try {
223                         if (handler.motionDetected) {
224                             output.sendSnapshotBasedFrame(handler.getSnapshot());
225                         } // every 8 seconds if no motion or the first three snapshots to fill any FIFO
226                         else if (counter % 8 == 0 || counter < 3) {
227                             output.sendSnapshotBasedFrame(handler.getSnapshot());
228                         }
229                         counter++;
230                         Thread.sleep(1000);
231                     } catch (InterruptedException | IOException e) {
232                         // Never stop streaming until IOException. Occurs when browser stops the stream.
233                         openAutoFpsStreams.removeStream(output);
234                         if (openAutoFpsStreams.isEmpty()) {
235                             handler.streamingAutoFps = false;
236                             logger.debug("All autofps.mjpeg streams have stopped.");
237                         }
238                         return;
239                     }
240                 } while (true);
241             case "/instar":
242                 InstarHandler instar = new InstarHandler(handler);
243                 instar.alarmTriggered(pathInfo + "?" + req.getQueryString());
244                 return;
245             default:
246                 if (pathInfo.endsWith(".ts")) {
247                     sendFile(resp, pathInfo, "video/MP2T");
248                 } else if (pathInfo.endsWith(".gif")) {
249                     sendFile(resp, pathInfo, "image/gif");
250                 } else if (pathInfo.endsWith(".jpg")) {
251                     // Allow access to the preroll and postroll jpg files
252                     sendFile(resp, pathInfo, "image/jpg");
253                 } else if (pathInfo.endsWith(".mp4")) {
254                     sendFile(resp, pathInfo, "video/mp4");
255                 }
256                 return;
257         }
258     }
259
260     @Override
261     protected void sendFile(HttpServletResponse response, String filename, String contentType) throws IOException {
262         // Ensure no files can be sourced from parent or child folders
263         String truncated = filename.substring(filename.lastIndexOf("/"));
264         super.sendFile(response, handler.cameraConfig.getFfmpegOutput() + truncated, contentType);
265     }
266
267     @Override
268     public void dispose() {
269         openStreams.closeAllStreams();
270         openSnapshotStreams.closeAllStreams();
271         openAutoFpsStreams.closeAllStreams();
272         super.dispose();
273     }
274 }