How to proxy a HTTP video stream to any amount of clients through a Spring Webserver

10,451

Solution 1

I am not sure what kind of source you are using for generating your video stream (live camera or a video file or youtube video or ..)

You can probably use StreamingResponseBody (requires Spring 4.2+). Refer following links

http://www.logicbig.com/tutorials/spring-framework/spring-web-mvc/streaming-response-body/

http://shazsterblog.blogspot.in/2016/02/asynchronous-streaming-request.html

Try this -

    @GetMapping("/stream1")
        @ResponseBody
        public StreamingResponseBody getVidoeStream1(@RequestParam String any) throws IOException {
            /* do security check before connecting to stream hosting server */ 
            RestTemplate restTemplate = new RestTemplate();
            ResponseEntity<Resource> responseEntity = restTemplate.exchange( "http://localhost:8080/stream", HttpMethod.GET, null, Resource.class );
            InputStream st = responseEntity.getBody().getInputStream();
            return (os) -> {
                readAndWrite(st, os);
            };


    }

private void readAndWrite(final InputStream is, OutputStream os)
            throws IOException {
        byte[] data = new byte[2048];
        int read = 0;
        while ((read = is.read(data)) > 0) {
            os.write(data, 0, read);
        }
        os.flush();
    }

It should work. You can write your own implementation of readAndWrite() depending on your requirements.

So, your spring proxy controller could be something like this...

@Controller
public class HttpStreamProxyController {

    @RequestMapping("/spring") 
    @ResponseBody
    public StreamingResponseBody  getSecuredHttpStream() {
       if (clientIsSecured) {
       //... Security information

    RestTemplate restTemplate = new RestTemplate();
    // get video stream by connecting to stream hosting server  like this
            ResponseEntity<Resource> responseEntity = restTemplate.exchange( "https://ur-to-stream", HttpMethod.GET, null, Resource.class );
            InputStream st = responseEntity.getBody().getInputStream();
    // Or if there is any other preferred way of getting the video stream use that. The idea is to get the video input stream    

    // now return a StreamingResponseBody  object created by following lambda 
            return (os) -> {
                readAndWrite(st, os);
            };

       } else {
          return null;
       }

   }
}

The StreamingResponseBody returned by your rest endpoint will work fine with HTML5, which could be something like ..

<video width="320" height="240" controls>
  <source src="/spring" type="video/mp4">
  Your browser does not support the video tag
</video>

Solution 2

I struggled with this same problem for days, attempting to migrate my node app to Spring/React. I'm using a raspberry pi running Motion, which acts as a remote stream server that I was previously able to use the node module mjpeg-proxy to easily proxy the way OP wishes. This thread was the most useful set of examples in my search to do this in Java, however none of the examples quite worked for me. I'm hoping my controller class and component will help others attempting the same.

Please take note of response.setContentType("multipart/x-mixed-replace; boundary=BoundaryString"); as the boundary=BoundaryString portion was the last critical piece. I opened the chrome debugger while connecting directly to the stream and this was in the response headers -- when I copied it, suddenly everything worked!


import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.servlet.http.HttpServletResponse;
import java.net.URI;

@RestController
@RequestMapping(value = "/video")
public class VideoController {

    Logger log = LoggerFactory.getLogger(VideoController.class);

    @RequestMapping("/oculus")
    public void oculus(HttpServletResponse response) {
        log.info("Calling /video/oculus...");
        RestTemplate restTemplate = new RestTemplate();
        restTemplate.execute(
                URI.create("http://oculus:8081"),
                HttpMethod.GET,
                (ClientHttpRequest request) -> {},
                responseExtractor -> {
                    response.setContentType("multipart/x-mixed-replace; boundary=BoundaryString");
                    IOUtils.copy(responseExtractor.getBody(), response.getOutputStream());
                    return null;
                }
        );
    }

    @RequestMapping("/door")
    public void door(HttpServletResponse response) {
        log.info("Calling /video/door...");

        RestTemplate restTemplate = new RestTemplate();
        restTemplate.execute(
                URI.create("http://vox:9002"),
                HttpMethod.GET,
                clientHttpRequest -> {
                    clientHttpRequest.getHeaders().add(HttpHeaders.AUTHORIZATION, "Basic blahblahBase64encodedUserAndPass=");
                },
                responseExtractor -> {
                    response.setContentType("multipart/x-mixed-replace; boundary=BoundaryString");
                    IOUtils.copy(responseExtractor.getBody(), response.getOutputStream());
                    return null;
                }
        );
    }

}

I have included examples for using Motion's basic auth, as well as an unprotected stream.

Both streams are then available on my app using the following bit of react js


class Video extends React.Component{
    render() {
        return (
            <div className="Content">
                <p>
                    <img className="stream" src="/video/oculus"/>
                </p>
                <p>
                    <img className="stream" src="/video/door"/>
                </p>
            </div>
        );
    }
}

export default Video;```

Solution 3

A good way to do video streaming in a Spring Boot application (I used a different source, however — a USB camera):

@GetMapping(value = "/stream")
public ResponseEntity<StreamingResponseBody> stream() {
    StreamingResponseBody stream = outputStream -> {
        while (streaming.get()) {
            final var raw = streamImage.get();
            final var jpeg = convert(byte2Buffered(raw.getImageData(), raw.getImageWidth(), raw.getImageHeight()));
            outputStream.write(jpeg);
        }
        outputStream.flush();
    };
    final var headers = new HttpHeaders();
    headers.add("Access-Control-Allow-Origin", "*");
    headers.add("Cache-Control", "no-cache, no-store, must-revalidate");
    headers.add("Content-Type", "multipart/x-mixed-replace;boundary=frame");
    headers.add("Expires", "0");
    headers.add("Pragma", "no-cache");
    headers.add("Max-Age", "0");
    return ResponseEntity.ok()
            .headers(headers)
            .body(stream);
}

This way you'll get a stream on URL endpoint, which you can embed into a web page using img HTML tag.

Share:
10,451
LeoColman
Author by

LeoColman

I'm a brazilian developer and student. Fanatic for the Kotlin language and the Kotest framework.

Updated on July 25, 2022

Comments

  • LeoColman
    LeoColman over 1 year

    Provided that I have a video HTTP stream broadcasted on a server that is on the same network as my Spring Webserver is, for instance in some url such as:

    http://localhost:9090/httpstream

    How can I proxy this video stream to any amount of clients, using Spring? The following example demonstrates the wanted flow:

    1. Spring webserver can be found at http://localhost:9091/spring
    2. A client wants to access a video stream, so he connects his video-stream-player to http://localhost:9091/spring (the spring webserver)
    3. The Spring WebServer should redirect the stream found on http://localhost:9090/httpstream to the client, with the latter never knowing that he accessed the httpstream host. The connection is made by Spring, not by the client

    This is needed because the HTTPStream is an unsecured and not authenticated host, and I wanted to wrap it around a Spring Webserver, so that I could use some form of Security, such as a Basic Auth.


    I tried requesting some form of mapping, but I couldn't find what kind of object to return, nor how to make the connection, but the expected behaviour should be something like this:

    @Controller
    public class HttpStreamProxyController {
    
        @RequestMapping("/spring") {
        public /*Stream Object?*/ getSecuredHttpStream() {
           if (clientIsSecured) {
           //... Security information
    
           return   //What should be returned?
           }
       }
    }
    
  • LeoColman
    LeoColman over 6 years
    This is likely correct, @vsoni, I'll give it a test and accept this as the answer.
  • LeoColman
    LeoColman over 6 years
    when trying restTemplate.exchange(..) the request hangs, and never go to the next line. Any ideas how to solve it?
  • vsoni
    vsoni over 6 years
    Are you getting any exception on the server? ... OR are you able to get data stream when you access your url from browser directly? I mean the url that you are trying to access thru restTemplate.exchange
  • LeoColman
    LeoColman over 6 years
    No errors, and the request never complete. after a while I get a 503 error on the source (accessing via VLC). I solved it using RestTemplate execute method, but am still studying the issue
  • Zhandos
    Zhandos over 3 years
    Could you provide more details of your solution. It's not clear origin of streaming.get() and streamImage.get(). I'm trying to achieve your approach, but struggling with returning of sequence of jpeg images from camera as live stream .
  • Poliakoff
    Poliakoff over 3 years
    @Zhandos streaming.get() is a variable of AtomicBoolean type which indicates whether there is data available in the stream. streamImage.get(); is a variable of class AtomicReference<V> implements java.io.Serializable which points to the array of the images. This class with the array of images is provided by the SDK which comes with the device. I suggest checking whether your device can provide something similar or not.
  • Zhandos
    Zhandos over 3 years
    Thank you for your answer, it helped me to achieve my goal.