توضیحات
وقتی سایت هایی مثل یوتیوب رو باز میکنیم و شروع به تماشای یک ویدیو میکنیم اگه دقت کرده باشید صفحه منتظر دانلود کامل ویدیو نمیشه و درحالی که ویدیو داره دانلود میشه میتونیم به طور همزمان فیلم رو تا جایی که دانلود شده تماشا کنیم؛ برای این موضوع سایت هایی مثل یوتیوب از روش خوندن تدریجی ویدیو همزمان با دانلود استفاده میکنن.
در اینجا میخوایم این روش رو بررسی کنیم و ببینیم سایت هایی مثل یوتیوب برای انجام این کار چکار میکنن.
در ادامه ابتدا هدر های مورد نیاز کلاینت به سرور (درخواست) و سرور به کلاینت (پاسخ) رو بررسی میکنیم و سپس به پیاده سازی هدر ها با کمک فریمورک اسپرینگ (spring) در جاوا می پردازیم.
توجه
پروژه ای که میخوایم انجام بدیم علاوه بر پخش تدریجی ویدیو برای دانلود فایل نیز قابل استفاده است.
هدر های سرور به کلاینت
هدر هایی که سرور به کلاینت ارسال می کنه عموما بهشون میگیم هدر های پاسخ (Response http headers)
در این بخش بعضی از این هدر ها رو که بدرد پروژه میخوره مورد بررسی قرار میدیم.
Content-Disposition
به کلاینت میگه محتوی رو بهصورت inline نمایش بده یا به صورت attachment دانلودش کن
مثال
Content-Disposition: inline; filename="name_of_the_file.extension"
Content-Range
به کلاینت میگه چه مقدار از فایل رو باید بخونه
مثال
Content-Range: bytes rangeEnd-rangeStart/fileSize
Accept-Range
به کلاینت میگه واحد Range قابل قبول چیه (bytes, kilobytes...)
مثال
Accept-Range: bytes
Content-Length
مقدار بایت هایی که سرور به کلاینت ارسال میکنه در این مورد طول رینج مقدار بایت ارسال شدست
مثال
Content-Length: 2048
Content-Type
نوع و فرمت فایل رو به کلاینت میگه
مثال
Content-Type: audio/mpeg
هدر های کلاینت به سرور
هدر هایی که کلاینت به سرور ارسال میکنه رو میگیم هدر های درخواستی (Request Http Headers)
در این قسمت هدر هایی رو که در پروژه میخوایم استفاده کنیم بررسی میکنیم.
Range
به سرور میگه چه قسمت هایی از فایل رو میخواد و سرور باید دقیقا همون رو به کلاینت پس بده
مثال:
Range: bytes rangeEnd-rangeStart
یا
Range: bytes rangeStart-
پیادهسازی header ها با اسپرینگ
حالا نوبت به استفاده از این هدر ها در کد است
ایجاد کلاس StreamingService
ابتدا منطق برنامه رو در کلاس StreamingService پیاده میکنیم.
StreamingService.java
import org.springframework.http.*;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@Service
public class StreamingService {
private final HttpStatus status = HttpStatus.PARTIAL_CONTENT;
@Async
public ResponseEntity<byte[]> stream(String filePath, String requestedRange) {
byte[] bytes;
HttpHeaders headers = new HttpHeaders();
headers.setContentDisposition(
ContentDisposition
.builder("inline")
.filename(getFileName(filePath))
.build());
headers.setContentType(
MediaTypeFactory
.getMediaType(getFileName(filePath))
.orElse(MediaType.APPLICATION_OCTET_STREAM));
try {
long fileSize = getFileSize(filePath);
//create Range instance
Range range = new Range("bytes", requestedRange, fileSize);
headers.setContentLength(range.rangeLength);
headers.add(HttpHeaders.CONTENT_RANGE,
range.getContentRange());
headers.add(HttpHeaders.ACCEPT_RANGES, range.acceptRanges);
bytes = getPartialContent(filePath, range);
} catch (IOException e) {
e.printStackTrace();
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
return new ResponseEntity<>(bytes, headers, status);
}
//read corresponding bytes with current range
private byte[] getPartialContent(String filePath, Range range) throws IOException {
Path path = Paths.get(filePath);
int length = (int) (range.arrayRange[1] - range.arrayRange[0]);
RandomAccessFile raf = new RandomAccessFile(path.toFile(), "r");
ByteBuffer buffer = ByteBuffer.allocate(length);
FileChannel channel = raf.getChannel();
channel.read(buffer, range.arrayRange[0]);
raf.close();
return buffer.array();
}
private long getFileSize(String filePath) throws IOException {
Path path = Paths.get(filePath);
return Files.size(path);
}
private String getFileName(String filePath) {
return Paths.get(filePath).getFileName().toString();
}
// Range class which splits requested range from the client and creates new range
private static class Range {
private final String acceptRanges;
private final long[] arrayRange;
private final long fileSize;
private long rangeLength = 2048;
public Range(String acceptRanges, String range, long fileSize) {
this.acceptRanges = acceptRanges;
this.fileSize = fileSize;
arrayRange = arrayRange(range);
}
public String getContentRange() {
return acceptRanges + " " + arrayRange[0] + "-" + arrayRange[1] + "/" + fileSize;
}
private long[] arrayRange(String range) {
long rangeStart, rangeEnd;
if (range == null) {
rangeStart = 0;
rangeEnd = rangeLength;
} else {
String[] ranges = range.split("[a-zA-Z=-]");
if (ranges.length > 7) {
rangeStart = Long.parseLong(ranges[6]);
rangeEnd = Long.parseLong(ranges[8]);
} else {
rangeEnd = Long.parseLong(ranges[6]) + rangeLength;
rangeStart = rangeEnd - rangeLength;
}
}
rangeEnd = Math.min(rangeEnd, fileSize);
rangeLength = rangeEnd - rangeStart;
return new long[]{rangeStart, rangeEnd};
}
}
}
صدا زدن و استفاده از کلاس ساخته شده در Controller
سپس از کلاس StreamingService در کلاس کنترلر استفاده میکنیم.
StreamingController.java
import org.springframework.http.HttpHeaders;
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.RequestMapping;
import webcontent.service.StreamingService;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
@Controller
@RequestMapping("/data")
public class StreamingController {
private final StreamingService service;
public StreamingController(StreamingService service) {
this.service = service;
}
@GetMapping("/stream/{name}")
public ResponseEntity<byte[]> streamMusic(
@RequestHeader(value = HttpHeaders.RANGE, required = false) String ranges,
@PathVariable("name") String name
) throws InterruptedException, ExecutionException {
String absoluteName = name + ".mp3";
String absolutePath = "/PATH_TO_FILE_DIRECTORY/" + absoluteName;
//Compute stream method asynchronously
return CompletableFuture.completedFuture(service.stream(absolutePath, ranges)).get();
}
}