Creation of a Java client app to show hls streaming from external source with Basic Auth.

huangapple go评论53阅读模式
英文:

Creation of a Java client app to to show hls streaming from external source with Basic Auth

问题

I'm trying to develop a simple java webapp with spring mvc that access an external HLS server that works like a proxy and streams more than a live video. The external server (rtsp-simple-server, here the github page for those who are interested: https://github.com/aler9/mediamtx) has only the HLS protocol enabled and every source streams served is accessible from the urls: https://mediaserver.url:port/stream1, https://mediaserver.url:port/stream2, etc... The streams are protected by the HTTP Basic Authentication.

My web app, deployed in my local machine need to access the streams, perform the authentication from the backend and show the video retrived in a web page. For the streaming part, in the frontend, I use the streaming client hls.js.

My web page look like this:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
html, body {
	margin: 0;
	padding: 0;
	height: 100%;
	overflow: hidden;
}
#video {
	width: 50%;
	height: 50%;
	background: black;
}
</style>
</head>
<body>

<video id="video" muted controls autoplay playsinline></video>

<script src="https://cdn.jsdelivr.net/npm/hls.js@1.2.9"></script>

<script>
const create = () => {
	const video = document.getElementById('video');
	var videoSrc = 'https://mediaserver.url:port/stream1/stream.m3u8';

	// always prefer hls.js over native HLS.
	// this is because some Android versions support native HLS
	// but don't support fMP4s.
	if (Hls.isSupported()) {
		const hls = new Hls();

		hls.on(Hls.Events.ERROR, (evt, data) => {
			if (data.fatal) {
				hls.destroy();

				setTimeout(create, 2000);
			}
		});

		hls.loadSource(videoSrc);
		hls.attachMedia(video);

		video play();
	} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
		// since it's not possible to detect timeout errors in iOS,
		// wait for the playlist to be available before starting the stream
		fetch(videoSrc)
			.then(() => {
				video.src = videoSrc;
				video.play();
			});
	}
};

window.addEventListener('DOMContentLoaded', create);

</script>

</body>
</html>

Is there a way in Spring to inject the basic authentication header from the backend so that the hls client just need to call the stream and show it in the page, so that the login popup doesn't show up, nor do I need to write the credentials inside the streaming URL in the web page?

I tried using webfilter, and I also tried to use the spring object RestTemplate. With the last one I managed to retrieve the m3u8 manifest but when I use it to retrieve the segments the browser asks me to save them locally.

Many thanks in advance.

Here the HTML code I get from the server in the video tag:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
html, body {
	margin: 0;
	padding: 0;
	height: 100%;
	overflow: hidden;
}
#video {
	width: 100%;
	height: 100%;
	background: black;
}
</style>
</head>
<body>

<video id="video" muted controls autoplay playsinline></video>

<script src="https://cdn.jsdelivr.net/npm/hls.js@1.2.9"></script>

<script>
const create = () => {
	const video = document.getElementById('video');

	// always prefer hls.js over native HLS.
	// this is because some Android versions support native HLS
	// but don't support fMP4s.
	if (Hls.isSupported()) {
		const hls = new Hls({
			maxLiveSyncPlaybackRate: 1.5,
		});

		hls.on(Hls.Events.ERROR, (evt, data) => {
			if (data.fatal) {
				hls.destroy();

				setTimeout(create, 2000);
			}
		});

		hls.loadSource('index.m3u8');
		hls.attachMedia(video);

		video play();
	} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
		// since it's not possible to detect timeout errors in iOS,
		// wait for the playlist to be available before starting the stream
		fetch('stream.m3u8')
			.then(() => {
				video.src = 'index.m3u8';
				video.play();
			});
	}
};

window.addEventListener('DOMContentLoaded', create);

</script>

</body>
</html>

I found the solution with Spring. Here the controller I've written to map the requests to the mediaserver:

@Controller
@EnableAsync
public class RTSPSSController {
	private static Logger logger = LogManager.getLogger(RTSPSSController.class);
	private static String HOST = "https://media.server.org/";

	@Autowired
	private RestTemplate restTemplate;

	@RequestMapping(value = "cameras")
	public String cameras() {
		return "cameras";
	}

	@RequestMapping(value = "cameras/{camera}")
	public String camera(@PathVariable("camera") String camera) {
		return "cameras/" + camera;
	}

	@RequestMapping(value = "streams/{camera}")
	@ResponseBody
	public StreamingResponseBody getStream(@PathVariable("camera") String camera) 
			throws IOException {
		try {
			return stream(camera, "");
		} catch (Exception e) {
			System.err.println("Error " + e.getMessage());
			return new ResponseEntity<StreamingResponseBody>(HttpStatus.BAD_REQUEST).getBody();
		}
	}

	@RequestMapping(value = "streams/{camera}/{media:.+}")
	@ResponseBody
	public StreamingResponseBody stream(@PathVariable("camera") String camera, @PathVariable("media") String media)
			throws IOException {
		try {
			List<ClientHttpRequestInterceptor> interceptors = restTemplate.getInterceptors();
			if (interceptors == null) {
				interceptors = Collections.emptyList();
			}
			interceptors = new ArrayList<>(interceptors);
			interceptors.removeIf(BasicAuthorizationInterceptor.class::isInstance);
			interceptors.add(new BasicAuthorizationInterceptor("user", "password"));
			restTemplate.setInterceptors(interceptors);
			ResponseEntity<Resource> responseEntity = restTemplate.exchange(
					HOST+ camera + "/" + media, HttpMethod.GET, null, Resource.class);

			InputStream st = responseEntity.getBody().getInputStream();

			return (os) -> {
				readAndWrite(st, os);
			};
		} catch (Exception e) {
			System.err.println("Error " + e.getMessage());
			return new ResponseEntity<StreamingResponseBody>(HttpStatus.BAD_REQUEST).getBody();
		}
	}

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

Now I can embed the streaming from the cameras served by the media server simply using the iframe tag as in the example down here:

<%@ page

<details>
<summary>英文:</summary>

I&#39;m trying to develop a simple java webapp with spring mvc that access an external HLS server that works like a proxy and streams more than a live video.
The external server (rtsp-simple-server, here the github page for those who are interested: https://github.com/aler9/mediamtx) has only the HLS protocol enabled and every source streams served is accessible from the urls: https://mediaserver.url:port/stream1, https://mediaserver.url:port/stream2, etc...
The streams are protected by the HTTP Basic Authentication.
My web app, deployed in my local machine need to access the streams, perform the authentication from the backend and show the video retrived in a web page.
For the streaming part, in the frontend, I use the streaming client hls.js.
My web page look like this:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
overflow: hidden;
}
#video {
width: 50%;
height: 50%;
background: black;
}
</style>
</head>
<body>

<video id="video" muted controls autoplay playsinline></video>

<script src="https://cdn.jsdelivr.net/npm/hls.js@1.2.9"></script>

<script>

const create = () => {
const video = document.getElementById('video');
var videoSrc = 'https://mediaserver.url:port/stream1/stream.m3u8';

// always prefer hls.js over native HLS.
// this is because some Android versions support native HLS
// but don&#39;t support fMP4s.
if (Hls.isSupported()) {
const hls = new Hls();
hls.on(Hls.Events.ERROR, (evt, data) =&gt; {
if (data.fatal) {
hls.destroy();
setTimeout(create, 2000);
}
});
hls.loadSource(videoSrc);
hls.attachMedia(video);
video.play();
} else if (video.canPlayType(&#39;application/vnd.apple.mpegurl&#39;)) {
// since it&#39;s not possible to detect timeout errors in iOS,
// wait for the playlist to be available before starting the stream
fetch(videoSrc)
.then(() =&gt; {
video.src = videoSrc;
video.play();
});
}

};

window.addEventListener('DOMContentLoaded', create);

</script>

</body>
</html>

Is there a way in Spring to injec the basic authentication header from the backend so that the hls client just need to call the stream and show it in the page, so that the login popup doesn&#39;t show up, nor do i need to write the credentials inside the streaming url in the web page?
I tried using webfilter, and I also tried to use the spring object RestTemplate.
With the last one i managed to retrice the m3u8 manifest but when I use it to retrive the segments thebrowser ask me to save them locally.
Many thanks in advance.
Here the HTML code I get from the server in the video tag

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
overflow: hidden;
}
#video {
width: 100%;
height: 100%;
background: black;
}
</style>
</head>
<body>

<video id="video" muted controls autoplay playsinline></video>

<script src="https://cdn.jsdelivr.net/npm/hls.js@1.2.9"></script>

<script>

const create = () => {
const video = document.getElementById('video');

// always prefer hls.js over native HLS.
// this is because some Android versions support native HLS
// but don&#39;t support fMP4s.
if (Hls.isSupported()) {
const hls = new Hls({
maxLiveSyncPlaybackRate: 1.5,
});
hls.on(Hls.Events.ERROR, (evt, data) =&gt; {
if (data.fatal) {
hls.destroy();
setTimeout(create, 2000);
}
});
hls.loadSource(&#39;index.m3u8&#39;);
hls.attachMedia(video);
video.play();
} else if (video.canPlayType(&#39;application/vnd.apple.mpegurl&#39;)) {
// since it&#39;s not possible to detect timeout errors in iOS,
// wait for the playlist to be available before starting the stream
fetch(&#39;stream.m3u8&#39;)
.then(() =&gt; {
video.src = &#39;index.m3u8&#39;;
video.play();
});
}

};

window.addEventListener('DOMContentLoaded', create);

</script>

</body>
</html>

-----------------------**UPDATES**--------------------------
I found the solution with spring.
Here the controller I&#39;ve written to map the requests to the mediaserver.

@Controller
@EnableAsync
public class RTSPSSController {
private static Logger logger = LogManager.getLogger(RTSPSSController.class);
// private static String LOCALHOST_PORT = "http://localhost:8080";
private static String HOST = "https://media.server.org/";

@Autowired
private RestTemplate restTemplate;
@RequestMapping(value = &quot;cameras&quot;)
public String cameras() {
return &quot;cameras&quot;;
}
@RequestMapping(value = &quot;cameras/{camera}&quot;)
public String camera(@PathVariable(&quot;camera&quot;) String camera) {
return &quot;cameras/&quot; + camera;
}
@RequestMapping(value = &quot;streams/{camera}&quot;)
@ResponseBody
public StreamingResponseBody getStream(@PathVariable(&quot;camera&quot;) String camera) 
throws IOException {
try {
return stream (camera, &quot;&quot;);
} catch (Exception e) {
System.err.println(&quot;Error &quot; + e.getMessage());
// return null;
return new ResponseEntity&lt;StreamingResponseBody&gt;(HttpStatus.BAD_REQUEST).getBody();
}
}
@RequestMapping(value = &quot;streams/{camera}/{media:.+}&quot;)
@ResponseBody
public StreamingResponseBody stream(@PathVariable(&quot;camera&quot;) String camera, @PathVariable(&quot;media&quot;) String media)
throws IOException {
try {
// Retrieve restTemplate and inject the credentials for basic auth
List&lt;ClientHttpRequestInterceptor&gt; interceptors = restTemplate.getInterceptors();
if (interceptors == null) {
interceptors = Collections.emptyList();
}
interceptors = new ArrayList&lt;&gt;(interceptors);
interceptors.removeIf(BasicAuthorizationInterceptor.class::isInstance);
interceptors.add(new BasicAuthorizationInterceptor(&quot;user&quot;, &quot;password&quot;));
restTemplate.setInterceptors(interceptors);
// restTemplate.getInterceptors().add(new BasicAuthorizationInterceptor(&quot;user&quot;,
// &quot;password&quot;));
ResponseEntity&lt;Resource&gt; responseEntity = restTemplate.exchange(
HOST+ camera + &quot;/&quot; + media, HttpMethod.GET, null, Resource.class);
InputStream st = responseEntity.getBody().getInputStream();
// System.out.println(st.
return (os) -&gt; {
readAndWrite(st, os);
};
} catch (Exception e) {
System.err.println(&quot;Error &quot; + e.getMessage());
// return null;
return new ResponseEntity&lt;StreamingResponseBody&gt;(HttpStatus.BAD_REQUEST).getBody();
}
}

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

Now I can embed the streaming from the cameras served by the media server simply using the iframe tag as in the example down here:

<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="ISO-8859-1">
<title>Cameras</title>
</head>
<body>
<iframe src="./streams/camera1/"></iframe>
<iframe src="./streams/camera2/"></iframe>
<iframe src="..."></iframe>
</body>
</html>


</details>
# 答案1
**得分**: 0
Basic auth is simply adding a header of the following form to an http request
Adding this header to the fetch is [simple][1].
fetch(videoSrc, { headers: { "Authorization": "Basic " + Base64encoded(username + ‘:’ + password) } }).then(() => { video.src = videoSrc; video.play(); });
However, adding it to hls.js calls looks more [difficult][2]. Use authentication basic header, not id and token.
Put the Base64encoded(username + ‘:’ + password) into the page via the Java page renderer (Thymeleaf, JSP, outputStream) you are using in springboot.
Adding the auth header to the html5 video component used to be possible but it has been blocked because it is seen as a way to pirate videos. This 
[3] has a work around but its not simple. [1]: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch [2]: https://stackoverflow.com/questions/56647256/hls-js-required-send-http-header [3]: https://github.com/sampotts/plyr/issues/1312 <details> <summary>英文:</summary> Basic auth is simply adding a header of the following form to an http request Adding this header to the fetch is [simple][1]. fetch(videoSrc, { headers: { &quot;Authorization&quot;: &quot;Basic &quot; + Base64encoded(username + ‘:’ + password) }) .then(() =&gt; { video.src = videoSrc; video.play(); }); However, adding it to hls.js calls looks more [difficult][2]. Use authentication basic header, not id and token. Put the Base64encoded(username + ‘:’ + password) into the page via the Java page renderer (Thymeleaf, JSP, outputStream) you are using in springboot. Adding the auth header to the html5 video component used to be possible but it has been blocked because it is seen as a way to pirate videos. This
[3] has a work around but its not simple. [1]: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch [2]: https://stackoverflow.com/questions/56647256/hls-js-required-send-http-header [3]: https://github.com/sampotts/plyr/issues/1312 </details>

huangapple
  • 本文由 发表于 2023年4月4日 05:56:32
  • 转载请务必保留本文链接:https://go.coder-hub.com/75924044.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定