2 * Copyright (c) 2010-2024 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;
20 import java.util.Dictionary;
21 import java.util.Hashtable;
24 import javax.servlet.AsyncContext;
25 import javax.servlet.ServletInputStream;
26 import javax.servlet.http.HttpServletRequest;
27 import javax.servlet.http.HttpServletResponse;
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;
39 * The {@link CameraServlet} is responsible for serving files for a single camera back to the Jetty server normally
42 * @author Matthew Skinner - Initial contribution
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"));
50 private final IpCameraHandler handler;
51 public OpenStreams openStreams = new OpenStreams();
52 private OpenStreams openSnapshotStreams = new OpenStreams();
53 private OpenStreams openAutoFpsStreams = new OpenStreams();
55 public CameraServlet(IpCameraHandler handler, HttpService httpService) {
56 super(handler, httpService, INIT_PARAMETERS);
57 this.handler = handler;
61 protected void doPost(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp) throws IOException {
62 if (req == null || resp == null) {
65 String pathInfo = req.getPathInfo();
66 if (pathInfo == null) {
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());
77 snapshotData = req.getInputStream();
78 handler.processSnapshot(snapshotData.readAllBytes());
82 handler.onvifCamera.eventRecieved(req.getReader().toString());
85 logger.debug("Recieved unknown request \tPOST:{}", pathInfo);
91 protected void doGet(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp) throws IOException {
92 if (req == null || resp == null) {
95 String pathInfo = req.getPathInfo();
96 if (pathInfo == null) {
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);
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();
115 localFfmpeg.setKeepAlive(8);
116 sendFile(resp, pathInfo, "application/x-mpegURL");
119 // Allow files to be created, or you get old m3u8 from the last time this ran.
121 Thread.sleep(HLS_STARTUP_DELAY_MS);
122 } catch (InterruptedException e) {
125 sendFile(resp, pathInfo, "application/x-mpegURL");
127 case "/ipcamera.mpd":
128 sendFile(resp, pathInfo, "application/dash+xml");
130 case "/ipcamera.gif":
131 sendFile(resp, pathInfo, "image/gif");
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());
140 handler.getSnapshot();
141 final AsyncContext acontext = req.startAsync(req, resp);
142 acontext.start(new Runnable() {
145 Instant startTime = Instant.now();
149 } catch (InterruptedException e) {
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());
161 case "/snapshots.mjpeg":
162 handler.streamingSnapshotMjpeg = true;
163 handler.startSnapshotPolling();
164 StreamOutput output = new StreamOutput(resp);
165 openSnapshotStreams.addStream(output);
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.");
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);
190 output = new StreamOutput(resp, handler.mjpegContentType);
192 } else if (handler.mjpegUri.isEmpty() || "ffmpeg".equals(handler.mjpegUri)) {
193 output = new StreamOutput(resp);
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();
200 output = new StreamOutput(resp, handler.mjpegContentType);
202 openStreams.addStream(output);
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;
220 handler.closeChannel(handler.getTinyUrl(handler.mjpegUri));
222 logger.debug("All ipcamera.mjpeg streams have stopped.");
226 } while (!openStreams.isEmpty());
227 case "/autofps.mjpeg":
228 handler.streamingAutoFps = true;
229 output = new StreamOutput(resp);
230 openAutoFpsStreams.addStream(output);
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());
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.");
255 InstarHandler instar = new InstarHandler(handler);
256 instar.alarmTriggered(pathInfo + "?" + req.getQueryString());
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");
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);
281 public void dispose() {
282 openStreams.closeAllStreams();
283 openSnapshotStreams.closeAllStreams();
284 openAutoFpsStreams.closeAllStreams();