feat: if `audiowaveform` is present, generate & cache peaks on backend
requires https://github.com/bbc/audiowaveform/ to be installed and on $PATHfeat/type-attributes
parent
b31ca05fdf
commit
9ea1eea3ea
|
@ -14,41 +14,68 @@ const COLOR: &str = "#dc322f"; // solarized red
|
||||||
|
|
||||||
impl<'a> Previewable for AudioPath<'a> {
|
impl<'a> Previewable for AudioPath<'a> {
|
||||||
fn get_thumbnail(&self, options: HashMap<String, String>) -> Result<Option<Vec<u8>>> {
|
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
|
// -i long_clip.mp3 -o long_clip.json --pixels-per-second 20 --bits 8
|
||||||
.get("color")
|
let audiowaveform_cmd = Command::new("audiowaveform")
|
||||||
.map(String::to_owned)
|
.args(["-i", &self.0.to_string_lossy()])
|
||||||
.unwrap_or_else(|| COLOR.into());
|
.args(["-o", &*outfile.path().to_string_lossy()])
|
||||||
let dimensions = options
|
.args(["--pixels-per-second", "20"])
|
||||||
.get("dimensions")
|
.args(["--bits", "8"])
|
||||||
.map(String::to_owned)
|
.output()?;
|
||||||
.unwrap_or_else(|| "860x256".into());
|
|
||||||
|
|
||||||
let thumbnail_cmd = Command::new("ffmpeg")
|
if !audiowaveform_cmd.status.success() {
|
||||||
.args(["-i", &self.0.to_string_lossy()])
|
return Err(anyhow!(
|
||||||
.args([
|
"Failed to retrieve file duration: {:?}",
|
||||||
"-filter_complex",
|
String::from_utf8_lossy(&audiowaveform_cmd.stderr)
|
||||||
&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() {
|
let mut buffer = Vec::new();
|
||||||
return Err(anyhow!(
|
outfile.as_file().read_to_end(&mut buffer)?;
|
||||||
"Failed to render thumbnail: {:?}",
|
Ok(Some(buffer))
|
||||||
String::from_utf8_lossy(&thumbnail_cmd.stderr)
|
}
|
||||||
));
|
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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -147,6 +147,7 @@
|
||||||
responsive: true,
|
responsive: true,
|
||||||
backend: "MediaElement",
|
backend: "MediaElement",
|
||||||
mediaControls: true,
|
mediaControls: true,
|
||||||
|
normalize: true,
|
||||||
xhr: { cache: "force-cache" },
|
xhr: { cache: "force-cache" },
|
||||||
plugins: [
|
plugins: [
|
||||||
TimelinePlugin.default.create({
|
TimelinePlugin.default.create({
|
||||||
|
@ -202,7 +203,16 @@
|
||||||
setTimeout(() => wavesurfer.setCurrentTime(region.start));
|
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>
|
</script>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue