Compare commits
2 Commits
debe714215
...
c4deff8386
Author | SHA1 | Date | |
---|---|---|---|
c4deff8386 | |||
3772c0ecb9 |
@ -14,4 +14,6 @@ tracing = "0.1.37"
|
|||||||
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
|
||||||
tower-http = { version = "0.5.2", features = ["trace"] }
|
tower-http = { version = "0.5.2", features = ["trace"] }
|
||||||
num_cpus = "1.16.0"
|
num_cpus = "1.16.0"
|
||||||
ffmpeg-next = "7.0.1"
|
ffmpeg-next = "7.0.1"
|
||||||
|
ureq = { version = "*", features = ["json", "charset"] }
|
||||||
|
mime_guess = "2.0.4"
|
75
Dockerfile.scratch
Normal file
75
Dockerfile.scratch
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
# Initially I wanted to build a slim image using this Dockerfile and static linking.
|
||||||
|
# It would use something like this in Cargo.toml:
|
||||||
|
#
|
||||||
|
# [build-dependencies.clang]
|
||||||
|
# version = "2.0.0"
|
||||||
|
# default-features = false
|
||||||
|
# features = ["static"]
|
||||||
|
#
|
||||||
|
# [build-dependencies.clang-sys]
|
||||||
|
# version = "1.7.0"
|
||||||
|
# default-features = false
|
||||||
|
# features = ["clang_16_0", "static"]
|
||||||
|
#
|
||||||
|
# But there are a lot of problems with static linking and clang & clang-sys crates:
|
||||||
|
# - https://github.com/KyleMayes/clang-sys/issues/174
|
||||||
|
# - https://github.com/KyleMayes/clang-rs/issues/17
|
||||||
|
# - https://github.com/KyleMayes/clang-sys/issues/148 (this is the one I *like* the most!)
|
||||||
|
#
|
||||||
|
# So, no static linking for now, but I don't wanna erase all this gorgeous Dockerfile code, so... I'll let it linger
|
||||||
|
# in the repo for now.
|
||||||
|
|
||||||
|
# Stage 1: Build the Rust application
|
||||||
|
FROM rust:1.78-alpine AS builder
|
||||||
|
|
||||||
|
# Set the working directory
|
||||||
|
WORKDIR /usr/src/
|
||||||
|
|
||||||
|
# Install necessary packages
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
ffmpeg-libs \
|
||||||
|
ffmpeg-dev \
|
||||||
|
pkgconf \
|
||||||
|
musl-dev \
|
||||||
|
clang16-static \
|
||||||
|
llvm-dev \
|
||||||
|
zlib-dev \
|
||||||
|
libffi-dev \
|
||||||
|
ncurses-dev \
|
||||||
|
gcc \
|
||||||
|
g++ \
|
||||||
|
git \
|
||||||
|
build-base
|
||||||
|
|
||||||
|
# Add target for musl
|
||||||
|
RUN rustup target add x86_64-unknown-linux-musl
|
||||||
|
|
||||||
|
# Create a new Rust project
|
||||||
|
RUN USER=root cargo new atranscoder-rpc
|
||||||
|
WORKDIR /usr/src/atranscoder-rpc
|
||||||
|
|
||||||
|
# Copy the manifest and lock files
|
||||||
|
COPY Cargo.toml Cargo.lock ./
|
||||||
|
|
||||||
|
# Set the environment variable for libclang path
|
||||||
|
ENV LIBCLANG_PATH=/usr/lib/llvm16/lib
|
||||||
|
ENV PKG_CONFIG_PATH=/usr/lib/pkgconfig
|
||||||
|
|
||||||
|
# Build dependencies to cache them
|
||||||
|
RUN cargo build --release --target x86_64-unknown-linux-musl
|
||||||
|
|
||||||
|
# Copy the source code
|
||||||
|
COPY src ./src
|
||||||
|
|
||||||
|
# Build the project and install the binary
|
||||||
|
RUN cargo install --target x86_64-unknown-linux-musl --path . && \
|
||||||
|
mkdir -p /result/{tmp,bin} && \
|
||||||
|
mv /usr/local/cargo/bin/atranscoder-rpc /result/bin
|
||||||
|
|
||||||
|
# Stage 2: Create the final minimal image
|
||||||
|
FROM scratch
|
||||||
|
COPY --from=builder /usr/bin/dumb-init /bin/
|
||||||
|
COPY --from=builder /result/bin /bin
|
||||||
|
COPY --from=builder /result/tmp /tmp
|
||||||
|
EXPOSE 8090
|
||||||
|
ENTRYPOINT ["/bin/dumb-init", "--", "/bin/atranscoder-rpc"]
|
@ -24,9 +24,9 @@ curl --location 'http://localhost:8090/enqueue' \
|
|||||||
- [ ] Implement acceptable error handling.
|
- [ ] Implement acceptable error handling.
|
||||||
- [ ] Restart threads in case of panic.
|
- [ ] Restart threads in case of panic.
|
||||||
- [ ] Remove old conversion results and input files every Nth hours.
|
- [ ] Remove old conversion results and input files every Nth hours.
|
||||||
- [ ] Remove input file after transcoding it.
|
- [x] Remove input file after transcoding it.
|
||||||
- [ ] Implement file upload to `uploadUrl` (if `Content-Type: application/json` then conversion was not successful and body contains an error info).
|
- [x] Implement file upload to `uploadUrl` (if `Content-Type: application/json` then conversion was not successful and body contains an error info).
|
||||||
- [ ] Remove transcoding result after uploading it to the `uploadUrl`.
|
- [x] Remove transcoding result after uploading it to the `uploadUrl`.
|
||||||
- [ ] (Optional) Make `uploadUrl` optional and allow the client to download the file on-demand.
|
- [ ] (Optional) Make `uploadUrl` optional and allow the client to download the file on-demand.
|
||||||
- [ ] Docker image for `amd64` and `aarch64`.
|
- [ ] Docker image for `amd64` and `aarch64`.
|
||||||
- [ ] Statically linked binary for Docker image & result docker image based on `scratch` (reduce image size).
|
- [ ] Statically linked binary for Docker image & result docker image based on `scratch` (reduce image size).
|
||||||
|
@ -22,7 +22,7 @@ async fn main() {
|
|||||||
env::var("NUM_WORKERS")
|
env::var("NUM_WORKERS")
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|val| val.parse::<usize>().ok())
|
.and_then(|val| val.parse::<usize>().ok())
|
||||||
.filter(|&val| val > 0)
|
.filter(|&val| val > 0),
|
||||||
);
|
);
|
||||||
let temp_dir = env::var("TEMP_DIR").unwrap_or_else(|_| {
|
let temp_dir = env::var("TEMP_DIR").unwrap_or_else(|_| {
|
||||||
env::temp_dir()
|
env::temp_dir()
|
||||||
|
97
src/task.rs
97
src/task.rs
@ -1,9 +1,16 @@
|
|||||||
use std::error::Error;
|
use crate::dto::ConvertResponse;
|
||||||
use crate::transcoder::{Transcoder, TranscoderParams};
|
use crate::transcoder::{Transcoder, TranscoderParams};
|
||||||
use ffmpeg_next::channel_layout::ChannelLayout;
|
use ffmpeg_next::channel_layout::ChannelLayout;
|
||||||
use ffmpeg_next::{format, Dictionary};
|
use ffmpeg_next::{format, Dictionary};
|
||||||
|
use mime_guess::from_path;
|
||||||
|
use std::error::Error;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Read;
|
||||||
|
use std::path::Path;
|
||||||
use tracing::{debug, error};
|
use tracing::{debug, error};
|
||||||
|
use ureq::Response;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct Task {
|
pub struct Task {
|
||||||
id: uuid::Uuid,
|
id: uuid::Uuid,
|
||||||
params: TaskParams,
|
params: TaskParams,
|
||||||
@ -15,6 +22,40 @@ impl Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn execute(self) -> Result<(), Box<dyn Error>> {
|
pub fn execute(self) -> Result<(), Box<dyn Error>> {
|
||||||
|
if let Err(err) = self.clone().transcode() {
|
||||||
|
std::fs::remove_file(Path::new(&self.params.input_path)).ok();
|
||||||
|
std::fs::remove_file(Path::new(&self.params.output_path)).ok();
|
||||||
|
send_error(
|
||||||
|
self.id,
|
||||||
|
format!("Couldn't transcode: {}", err).as_str(),
|
||||||
|
&self.params.upload_url,
|
||||||
|
)
|
||||||
|
.ok();
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::fs::remove_file(Path::new(&self.params.input_path)).ok();
|
||||||
|
|
||||||
|
if let Err(err) = upload_file(&self.params.output_path, &self.params.upload_url) {
|
||||||
|
error!(
|
||||||
|
"couldn't upload result for job id={}, file path {}: {}",
|
||||||
|
&self.id.to_string(),
|
||||||
|
&self.params.output_path,
|
||||||
|
err
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
debug!(
|
||||||
|
"job id={} result was uploaded to {}",
|
||||||
|
&self.id.to_string(),
|
||||||
|
&self.params.upload_url
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::fs::remove_file(Path::new(&self.params.output_path)).ok();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn transcode(self) -> Result<(), Box<dyn Error>> {
|
||||||
debug!(
|
debug!(
|
||||||
"performing transcoding for task with id: {}",
|
"performing transcoding for task with id: {}",
|
||||||
self.id.to_string()
|
self.id.to_string()
|
||||||
@ -27,7 +68,7 @@ impl Task {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let octx = if let Some(codec_opts) = self.params.codec_opts {
|
let octx = if let Some(codec_opts) = &self.params.codec_opts {
|
||||||
format::output_as_with(
|
format::output_as_with(
|
||||||
&self.params.output_path,
|
&self.params.output_path,
|
||||||
&self.params.format,
|
&self.params.format,
|
||||||
@ -136,6 +177,7 @@ impl Task {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct TaskParams {
|
pub struct TaskParams {
|
||||||
pub format: String,
|
pub format: String,
|
||||||
pub codec: String,
|
pub codec: String,
|
||||||
@ -149,6 +191,57 @@ pub struct TaskParams {
|
|||||||
pub upload_url: String,
|
pub upload_url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn send_error(
|
||||||
|
id: uuid::Uuid,
|
||||||
|
error: &str,
|
||||||
|
url: &str,
|
||||||
|
) -> Result<Response, Box<dyn std::error::Error>> {
|
||||||
|
let response = ureq::post(url)
|
||||||
|
.set("Content-Type", "application/json")
|
||||||
|
.send_json(ConvertResponse {
|
||||||
|
id: Some(id.to_string()),
|
||||||
|
error: Some(error.to_string()),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if response.status() == 200 {
|
||||||
|
Ok(response)
|
||||||
|
} else {
|
||||||
|
Err(format!("Failed to send an error. Status: {}", response.status()).into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn upload_file<P: AsRef<Path>>(
|
||||||
|
file_path: P,
|
||||||
|
url: &str,
|
||||||
|
) -> Result<Response, Box<dyn std::error::Error>> {
|
||||||
|
let path = file_path.as_ref();
|
||||||
|
let file_name = path
|
||||||
|
.file_name()
|
||||||
|
.ok_or("Invalid file path")?
|
||||||
|
.to_str()
|
||||||
|
.ok_or("Invalid file name")?;
|
||||||
|
|
||||||
|
let mut file = File::open(path)?;
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
file.read_to_end(&mut buffer)?;
|
||||||
|
|
||||||
|
let mime_type = from_path(path).first_or_octet_stream();
|
||||||
|
|
||||||
|
let response = ureq::post(url)
|
||||||
|
.set("Content-Type", mime_type.as_ref())
|
||||||
|
.set(
|
||||||
|
"Content-Disposition",
|
||||||
|
&format!("attachment; filename=\"{}\"", file_name),
|
||||||
|
)
|
||||||
|
.send_bytes(&buffer)?;
|
||||||
|
|
||||||
|
if response.status() == 200 {
|
||||||
|
Ok(response)
|
||||||
|
} else {
|
||||||
|
Err(format!("Failed to upload file. Status: {}", response.status()).into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn params_to_avdictionary(input: &str) -> Dictionary {
|
fn params_to_avdictionary(input: &str) -> Dictionary {
|
||||||
let mut dict: Dictionary = Dictionary::new();
|
let mut dict: Dictionary = Dictionary::new();
|
||||||
for pair in input.split(';') {
|
for pair in input.split(';') {
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
|
use ffmpeg_next::log::Level;
|
||||||
use std::sync::mpsc::{self, Receiver, Sender};
|
use std::sync::mpsc::{self, Receiver, Sender};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use ffmpeg_next::log::Level;
|
|
||||||
|
|
||||||
use tracing::{debug, error};
|
use tracing::{debug, error};
|
||||||
|
|
||||||
|
@ -141,21 +141,16 @@ impl Transcoder {
|
|||||||
let mut source = ctx.source();
|
let mut source = ctx.source();
|
||||||
source.add(frame)
|
source.add(frame)
|
||||||
} else {
|
} else {
|
||||||
Err(ffmpeg::Error::Other {
|
Err(ffmpeg::Error::Other { errno: 0 })
|
||||||
errno: 0,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub(crate) fn flush_filter(&mut self) -> Result<(), ffmpeg::Error> {
|
pub(crate) fn flush_filter(&mut self) -> Result<(), ffmpeg::Error> {
|
||||||
if let Some(mut ctx) = self.filter.get("in") {
|
if let Some(mut ctx) = self.filter.get("in") {
|
||||||
let mut source = ctx.source();
|
let mut source = ctx.source();
|
||||||
source.flush()
|
source.flush()
|
||||||
} else {
|
} else {
|
||||||
Err(ffmpeg::Error::Other {
|
Err(ffmpeg::Error::Other { errno: 0 })
|
||||||
errno: 0,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user