feat: if `audiowaveform` is present, generate & cache peaks on backend

requires https://github.com/bbc/audiowaveform/ to be installed and on $PATH
feat/type-attributes
Tomáš Mládek 2022-09-19 22:27:20 +02:00
parent b31ca05fdf
commit 9ea1eea3ea
2 changed files with 70 additions and 33 deletions

View File

@ -14,41 +14,68 @@ const COLOR: &str = "#dc322f"; // solarized red
impl<'a> Previewable for AudioPath<'a> {
fn get_thumbnail(&self, options: HashMap<String, String>) -> Result<Option<Vec<u8>>> {
let outfile = tempfile::Builder::new().suffix(".webp").tempfile()?;
match options.get("type").map(|x| x.as_str()) {
Some("json") => {
let outfile = tempfile::Builder::new().suffix(".json").tempfile()?;
let color = options
.get("color")
.map(String::to_owned)
.unwrap_or_else(|| COLOR.into());
let dimensions = options
.get("dimensions")
.map(String::to_owned)
.unwrap_or_else(|| "860x256".into());
// -i long_clip.mp3 -o long_clip.json --pixels-per-second 20 --bits 8
let audiowaveform_cmd = Command::new("audiowaveform")
.args(["-i", &self.0.to_string_lossy()])
.args(["-o", &*outfile.path().to_string_lossy()])
.args(["--pixels-per-second", "20"])
.args(["--bits", "8"])
.output()?;
let thumbnail_cmd = Command::new("ffmpeg")
.args(["-i", &self.0.to_string_lossy()])
.args([
"-filter_complex",
&format!(
"[0:a]aformat=channel_layouts=mono, compand=gain=-2,
showwavespic=s={dimensions}:colors={color},
drawbox=x=(iw-w)/2:y=(ih-h)/2:w=iw:h=1:color={color}"
),
])
.args(["-vframes", "1"])
.arg(&*outfile.path().to_string_lossy())
.arg("-y")
.output()?;
if !audiowaveform_cmd.status.success() {
return Err(anyhow!(
"Failed to retrieve file duration: {:?}",
String::from_utf8_lossy(&audiowaveform_cmd.stderr)
));
}
if !thumbnail_cmd.status.success() {
return Err(anyhow!(
"Failed to render thumbnail: {:?}",
String::from_utf8_lossy(&thumbnail_cmd.stderr)
));
let mut buffer = Vec::new();
outfile.as_file().read_to_end(&mut buffer)?;
Ok(Some(buffer))
}
Some("image") | None => {
let outfile = tempfile::Builder::new().suffix(".webp").tempfile()?;
let color = options
.get("color")
.map(String::to_owned)
.unwrap_or_else(|| COLOR.into());
let dimensions = options
.get("dimensions")
.map(String::to_owned)
.unwrap_or_else(|| "860x256".into());
let thumbnail_cmd = Command::new("ffmpeg")
.args(["-i", &self.0.to_string_lossy()])
.args([
"-filter_complex",
&format!(
"[0:a]aformat=channel_layouts=mono, compand=gain=-2,
showwavespic=s={dimensions}:colors={color},
drawbox=x=(iw-w)/2:y=(ih-h)/2:w=iw:h=1:color={color}"
),
])
.args(["-vframes", "1"])
.arg(&*outfile.path().to_string_lossy())
.arg("-y")
.output()?;
if !thumbnail_cmd.status.success() {
return Err(anyhow!(
"Failed to render thumbnail: {:?}",
String::from_utf8_lossy(&thumbnail_cmd.stderr)
));
}
let mut buffer = Vec::new();
outfile.as_file().read_to_end(&mut buffer)?;
Ok(Some(buffer))
}
Some(_) => Err(anyhow!("type has to be one of: image, json")),
}
let mut buffer = Vec::new();
outfile.as_file().read_to_end(&mut buffer)?;
Ok(Some(buffer))
}
}

View File

@ -147,6 +147,7 @@
responsive: true,
backend: "MediaElement",
mediaControls: true,
normalize: true,
xhr: { cache: "force-cache" },
plugins: [
TimelinePlugin.default.create({
@ -202,7 +203,16 @@
setTimeout(() => wavesurfer.setCurrentTime(region.start));
});
wavesurfer.load(`${API_URL}/raw/${address}`);
try {
const peaksReq = await fetch(`${API_URL}/thumb/${address}?type=json`);
const peaks = await peaksReq.json();
wavesurfer.load(`${API_URL}/raw/${address}`, peaks.data);
} catch (e) {
console.warn(
`Failed to load peaks from server (${e}), falling back to client-side render...`
);
wavesurfer.load(`${API_URL}/raw/${address}`);
}
});
</script>