2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.ipcamera.internal.servlet;
15 import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.HLS_STARTUP_DELAY_MS;
17 import java.io.IOException;
18 import java.time.Duration;
19 import java.time.Instant;
21 import javax.servlet.AsyncContext;
22 import javax.servlet.ServletInputStream;
23 import javax.servlet.http.HttpServletRequest;
24 import javax.servlet.http.HttpServletResponse;
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;
36 * The {@link CameraServlet} is responsible for serving files for a single camera back to the Jetty server normally
39 * @author Matthew Skinner - Initial contribution
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();
49 public CameraServlet(IpCameraHandler handler, HttpService httpService) {
50 super(handler, httpService);
51 this.handler = handler;
55 protected void doPost(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp) throws IOException {
56 if (req == null || resp == null) {
59 String pathInfo = req.getPathInfo();
60 if (pathInfo == null) {
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());
71 snapshotData = req.getInputStream();
72 handler.processSnapshot(snapshotData.readAllBytes());
76 handler.onvifCamera.eventRecieved(req.getReader().toString());
79 logger.debug("Recieved unknown request \tPOST:{}", pathInfo);
85 protected void doGet(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp) throws IOException {
86 if (req == null || resp == null) {
89 String pathInfo = req.getPathInfo();
90 if (pathInfo == null) {
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);
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();
109 localFfmpeg.setKeepAlive(8);
110 sendFile(resp, pathInfo, "application/x-mpegURL");
113 // Allow files to be created, or you get old m3u8 from the last time this ran.
115 Thread.sleep(HLS_STARTUP_DELAY_MS);
116 } catch (InterruptedException e) {
119 sendFile(resp, pathInfo, "application/x-mpegURL");
121 case "/ipcamera.mpd":
122 sendFile(resp, pathInfo, "application/dash+xml");
124 case "/ipcamera.gif":
125 sendFile(resp, pathInfo, "image/gif");
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());
134 handler.getSnapshot();
135 final AsyncContext acontext = req.startAsync(req, resp);
136 acontext.start(new Runnable() {
139 Instant startTime = Instant.now();
143 } catch (InterruptedException e) {
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());
155 case "/snapshots.mjpeg":
156 handler.streamingSnapshotMjpeg = true;
157 handler.startSnapshotPolling();
158 StreamOutput output = new StreamOutput(resp);
159 openSnapshotStreams.addStream(output);
162 output.sendSnapshotBasedFrame(handler.getSnapshot());
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.");
175 case "/ipcamera.mjpeg":
176 if (handler.mjpegUri.isEmpty() || "ffmpeg".equals(handler.mjpegUri)) {
177 if (openStreams.isEmpty()) {
178 handler.setupFfmpegFormat(FFmpegFormat.MJPEG);
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);
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();
193 output = new StreamOutput(resp, handler.mjpegContentType);
194 openStreams.addStream(output);
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();
209 handler.closeChannel(handler.getTinyUrl(handler.mjpegUri));
211 logger.debug("All ipcamera.mjpeg streams have stopped.");
215 } while (!openStreams.isEmpty());
216 case "/autofps.mjpeg":
217 handler.streamingAutoFps = true;
218 output = new StreamOutput(resp);
219 openAutoFpsStreams.addStream(output);
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());
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.");
242 InstarHandler instar = new InstarHandler(handler);
243 instar.alarmTriggered(pathInfo + "?" + req.getQueryString());
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");
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);
268 public void dispose() {
269 openStreams.closeAllStreams();
270 openSnapshotStreams.closeAllStreams();
271 openAutoFpsStreams.closeAllStreams();