feat(backend): serve video with HTTP range requests
This commit is contained in:
@@ -120,6 +120,25 @@ impl FileRepository {
|
|||||||
Ok(stream)
|
Ok(stream)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn stream_part(
|
||||||
|
&self,
|
||||||
|
file_id: i64,
|
||||||
|
user_id: i64,
|
||||||
|
from: u64,
|
||||||
|
to: u64,
|
||||||
|
) -> Result<impl Stream<Item = std::io::Result<Bytes>> + use<>, LoftError> {
|
||||||
|
info!(
|
||||||
|
"Fetching metadata of file \"{}\" from file_records",
|
||||||
|
file_id
|
||||||
|
);
|
||||||
|
let record = self.get_file(file_id, user_id).await?;
|
||||||
|
info!("Streaming chunk of file \"{}\"", file_id);
|
||||||
|
let reader = self.op.reader(&record.storage_key).await.unwrap();
|
||||||
|
let stream = reader.into_bytes_stream(from..to).await.unwrap();
|
||||||
|
|
||||||
|
Ok(stream)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_file(&self, file_id: i64, user_id: i64) -> Result<FileRecord, LoftError> {
|
pub async fn get_file(&self, file_id: i64, user_id: i64) -> Result<FileRecord, LoftError> {
|
||||||
info!(
|
info!(
|
||||||
"Fetching metadata of file \"{}\" from file_records",
|
"Fetching metadata of file \"{}\" from file_records",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use axum::{
|
|||||||
Json, Router,
|
Json, Router,
|
||||||
body::Body,
|
body::Body,
|
||||||
extract::{Multipart, Path, State},
|
extract::{Multipart, Path, State},
|
||||||
|
http::{HeaderMap, Response, StatusCode, header},
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
routing::get,
|
routing::get,
|
||||||
};
|
};
|
||||||
@@ -19,6 +20,7 @@ pub fn routes_file(file_repository: FileRepository) -> Router {
|
|||||||
.route("/files", get(list_files).post(upload_file))
|
.route("/files", get(list_files).post(upload_file))
|
||||||
.route("/files/{id}", get(get_file).delete(delete_file))
|
.route("/files/{id}", get(get_file).delete(delete_file))
|
||||||
.route("/files/{id}/download", get(download_file))
|
.route("/files/{id}/download", get(download_file))
|
||||||
|
.route("/files/{id}/stream_part", get(stream_part))
|
||||||
.with_state(file_repository)
|
.with_state(file_repository)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +56,6 @@ async fn upload_file(
|
|||||||
Err(LoftError::UndefinedErrorType)
|
Err(LoftError::UndefinedErrorType)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[axum::debug_handler]
|
|
||||||
async fn get_file(
|
async fn get_file(
|
||||||
State(file_repository): State<FileRepository>,
|
State(file_repository): State<FileRepository>,
|
||||||
ctx: Ctx,
|
ctx: Ctx,
|
||||||
@@ -66,7 +67,6 @@ async fn get_file(
|
|||||||
Ok(Json(record))
|
Ok(Json(record))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[axum::debug_handler]
|
|
||||||
async fn download_file(
|
async fn download_file(
|
||||||
State(file_repository): State<FileRepository>,
|
State(file_repository): State<FileRepository>,
|
||||||
ctx: Ctx,
|
ctx: Ctx,
|
||||||
@@ -78,6 +78,49 @@ async fn download_file(
|
|||||||
Ok(Body::from_stream(stream))
|
Ok(Body::from_stream(stream))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn stream_part(
|
||||||
|
State(file_repository): State<FileRepository>,
|
||||||
|
ctx: Ctx,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Path(file_id): Path<u64>,
|
||||||
|
) -> Result<impl IntoResponse, LoftError> {
|
||||||
|
info!("stream_part");
|
||||||
|
let file_record = file_repository
|
||||||
|
.get_file(file_id as i64, ctx.user_id())
|
||||||
|
.await?;
|
||||||
|
let file_size = file_record.size as u64;
|
||||||
|
let (start, end): (u64, u64) = parse_range(&headers, file_size);
|
||||||
|
|
||||||
|
let stream = file_repository
|
||||||
|
.stream_part(file_id as i64, ctx.user_id(), start, end + 1)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let respones = Response::builder()
|
||||||
|
.status(StatusCode::PARTIAL_CONTENT)
|
||||||
|
.header(header::CONTENT_TYPE, file_record.file_type)
|
||||||
|
.header(header::CONTENT_LENGTH, end - start + 1)
|
||||||
|
.header(
|
||||||
|
header::CONTENT_RANGE,
|
||||||
|
format!("bytes {start}-{end}/{file_size}"),
|
||||||
|
)
|
||||||
|
.header(header::ACCEPT_RANGES, "bytes")
|
||||||
|
.body(Body::from_stream(stream))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Ok(respones)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_range(headers: &HeaderMap, file_size: u64) -> (u64, u64) {
|
||||||
|
let range = headers.get(header::RANGE);
|
||||||
|
let str = range.unwrap().to_str().unwrap();
|
||||||
|
let strip = str.strip_prefix("bytes=").unwrap();
|
||||||
|
let tuple = strip.split_once("-").unwrap();
|
||||||
|
let start = tuple.0.parse::<u64>().unwrap();
|
||||||
|
let end = tuple.1.parse::<u64>().unwrap_or(file_size - 1);
|
||||||
|
|
||||||
|
(start, end)
|
||||||
|
}
|
||||||
|
|
||||||
async fn delete_file(
|
async fn delete_file(
|
||||||
State(file_repository): State<FileRepository>,
|
State(file_repository): State<FileRepository>,
|
||||||
ctx: Ctx,
|
ctx: Ctx,
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
onclick={(e) => e.stopPropagation()}
|
onclick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{#if isVideo}
|
{#if isVideo}
|
||||||
<VideoPreview src={`/api/files/${file.id}/download`} />
|
<VideoPreview src={`/api/files/${file.id}/stream_part`} />
|
||||||
<div
|
<div
|
||||||
class="pointer-events-none absolute inset-x-0 top-0 z-10 flex items-center justify-between bg-linear-to-b from-black/70 to-transparent px-4 py-3"
|
class="pointer-events-none absolute inset-x-0 top-0 z-10 flex items-center justify-between bg-linear-to-b from-black/70 to-transparent px-4 py-3"
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user