2 * Copyright (c) 2010-2023 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());
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.");
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);
184 output = new StreamOutput(resp, handler.mjpegContentType);
187 if (handler.mjpegUri.isEmpty() || "ffmpeg".equals(handler.mjpegUri)) {
188 output = new StreamOutput(resp);
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();
195 output = new StreamOutput(resp, handler.mjpegContentType);
198 openStreams.addStream(output);
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();
213 handler.closeChannel(handler.getTinyUrl(handler.mjpegUri));
215 logger.debug("All ipcamera.mjpeg streams have stopped.");
219 } while (!openStreams.isEmpty());
220 case "/autofps.mjpeg":
221 handler.streamingAutoFps = true;
222 output = new StreamOutput(resp);
223 openAutoFpsStreams.addStream(output);
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());
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.");
248 InstarHandler instar = new InstarHandler(handler);
249 instar.alarmTriggered(pathInfo + "?" + req.getQueryString());
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");
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);
274 public void dispose() {
275 openStreams.closeAllStreams();
276 openSnapshotStreams.closeAllStreams();
277 openAutoFpsStreams.closeAllStreams();