First solution: Using FileSystemResource
FileSystemResource internally handles byte-range header support, reading and writing the appropriate headers.
Two problems with this approach.
It uses FileInputStream
internally for reading files. This is fine for small files, but not for large files served through byte-range requests. FileInputStream
will read the file from the beginning and discard the not needed content until it reches the requested start offset. This can cause slowdowns with larger files.
It sets "application/json"
as the "Content-Type"
response header. So, I provide my own "Content-Type"
header. See this thread
import java.nio.file.Path;
import java.nio.file.Paths;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class Stream3 {
@GetMapping(value = "/play_video/{video_id}")
@ResponseBody
public ResponseEntity<FileSystemResource> stream(@PathVariable("video_id") String video_id) {
String filePathString = "/opt/videos/" + video_id + ".mp4";
final HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.add("Content-Type", "video/mp4");
return new ResponseEntity<>(new FileSystemResource(filePathString), responseHeaders, HttpStatus.OK);
}
}
Second solution: Using HttpServletResponse
and RandomAccessFile
With RandomAccessFile
you can implement support for byte-range requests. The advantage over FileInputStream
, is that you don't need to read the file from the beginning every time there is a new range request, making this method usable also for larger files. RandomAccessFile
has a method called seek(long)
which calls the C method fseek()
, which directly moves the pointer for the file to the requested offset.
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import javax.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class Stream {
@GetMapping(value = "/play_video/{video_id}")
@ResponseBody
public void stream(
@PathVariable("video_id") String video_id,
@RequestHeader(value = "Range", required = false) String rangeHeader,
HttpServletResponse response) {
try {
OutputStream os = response.getOutputStream();
long rangeStart = 0;
long rangeEnd;
String filePathString = "/opt/videos/" + video_id + ".mp4";
Path filePath = Paths.get(filePathString);
Long fileSize = Files.size(filePath);
byte[] buffer = new byte[1024];
RandomAccessFile file = new RandomAccessFile(filePathString, "r");
try (file) {
if (rangeHeader == null) {
response.setHeader("Content-Type", "video/mp4");
response.setHeader("Content-Length", fileSize.toString());
response.setStatus(HttpStatus.OK.value());
long pos = rangeStart;
file.seek(pos);
while (pos < fileSize - 1) {
file.read(buffer);
os.write(buffer);
pos += buffer.length;
}
os.flush();
return;
}
String[] ranges = rangeHeader.split("-");
rangeStart = Long.parseLong(ranges[0].substring(6));
if (ranges.length > 1) {
rangeEnd = Long.parseLong(ranges[1]);
} else {
rangeEnd = fileSize - 1;
}
if (fileSize < rangeEnd) {
rangeEnd = fileSize - 1;
}
String contentLength = String.valueOf((rangeEnd - rangeStart) + 1);
response.setHeader("Content-Type", "video/mp4");
response.setHeader("Content-Length", contentLength);
response.setHeader("Accept-Ranges", "bytes");
response.setHeader("Content-Range", "bytes" + " " + rangeStart + "-" + rangeEnd + "/" + fileSize);
response.setStatus(HttpStatus.PARTIAL_CONTENT.value());
long pos = rangeStart;
file.seek(pos);
while (pos < rangeEnd) {
file.read(buffer);
os.write(buffer);
pos += buffer.length;
}
os.flush();
}
} catch (FileNotFoundException e) {
response.setStatus(HttpStatus.NOT_FOUND.value());
} catch (IOException e) {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
}
}
}
Third solution: Also using RandomAccessFile
, but StreamingResponseBody
instead of HttpServletResponse
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
@Controller
public class Stream2 {
@GetMapping(value = "/play_video/{video_id}")
@ResponseBody
public ResponseEntity<StreamingResponseBody> stream(
@PathVariable("video_id") String video_id,
@RequestHeader(value = "Range", required = false) String rangeHeader) {
try {
StreamingResponseBody responseStream;
String filePathString = "/opt/videos/" + video_id + ".mp4";
Path filePath = Paths.get(filePathString);
Long fileSize = Files.size(filePath);
byte[] buffer = new byte[1024];
final HttpHeaders responseHeaders = new HttpHeaders();
if (rangeHeader == null) {
responseHeaders.add("Content-Type", "video/mp4");
responseHeaders.add("Content-Length", fileSize.toString());
responseStream = os -> {
RandomAccessFile file = new RandomAccessFile(filePathString, "r");
try (file) {
long pos = 0;
file.seek(pos);
while (pos < fileSize - 1) {
file.read(buffer);
os.write(buffer);
pos += buffer.length;
}
os.flush();
} catch (Exception e) {}
};
return new ResponseEntity<>(responseStream, responseHeaders, HttpStatus.OK);
}
String[] ranges = rangeHeader.split("-");
Long rangeStart = Long.parseLong(ranges[0].substring(6));
Long rangeEnd;
if (ranges.length > 1) {
rangeEnd = Long.parseLong(ranges[1]);
} else {
rangeEnd = fileSize - 1;
}
if (fileSize < rangeEnd) {
rangeEnd = fileSize - 1;
}
String contentLength = String.valueOf((rangeEnd - rangeStart) + 1);
responseHeaders.add("Content-Type", "video/mp4");
responseHeaders.add("Content-Length", contentLength);
responseHeaders.add("Accept-Ranges", "bytes");
responseHeaders.add("Content-Range", "bytes" + " " + rangeStart + "-" + rangeEnd + "/" + fileSize);
final Long _rangeEnd = rangeEnd;
responseStream = os -> {
RandomAccessFile file = new RandomAccessFile(filePathString, "r");
try (file) {
long pos = rangeStart;
file.seek(pos);
while (pos < _rangeEnd) {
file.read(buffer);
os.write(buffer);
pos += buffer.length;
}
os.flush();
} catch (Exception e) {}
};
return new ResponseEntity<>(responseStream, responseHeaders, HttpStatus.PARTIAL_CONTENT);
} catch (FileNotFoundException e) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
} catch (IOException e) {
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
In your component.ts:
You can change the currently displaying video with playVideoFile()
export class AppComponent implements OnInit {
videoSources: Plyr.Source[];
ngOnInit(): void {
const fileName = 'sample';
this.playVideoFile(fileName);
}
playVideoFile(fileName: string) {
this.videoSources = [
{
src: `http://localhost:8080/play_video/${fileName}`,
},
];
}
}
And the html:
<div
#plyr
plyr
[plyrPlaysInline]="false"
[plyrSources]="videoSources"
></div>