use anyhow::{anyhow, Context};
use duct::cmd;
use log::*;
use scopeguard::defer;
use std::path::Path;
use std::{
fmt,
fs::{self, File},
io::{BufRead, BufReader},
process::Output,
str::{from_utf8, FromStr},
};
use super::interval::{Frame, Time};
const FFMS2_INDEX_EXT: &str = "ffindex";
const FFMS2_TIMES_INDEX_EXT: &str = "ffindex_track00.tc.txt";
const FFMS2_KEY_FRAMES_INDEX_EXT: &str = "ffindex_track00.kf.txt";
#[derive(Clone, Default, PartialEq)]
pub enum StreamType {
Audio,
Video,
#[default]
Unknown,
}
impl fmt::Display for StreamType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}",
match self {
StreamType::Audio => "audio",
StreamType::Video => "video",
StreamType::Unknown => "unknown",
}
)
}
}
impl FromStr for StreamType {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"audio" => StreamType::Audio,
"video" => StreamType::Video,
_ => StreamType::Unknown,
})
}
}
type Codec = Option<String>;
#[derive(Default, serde::Deserialize)]
pub struct Stream {
pub index: usize,
pub codec_type: Option<String>,
#[serde(alias = "codec_name")]
pub codec: Codec,
}
impl Stream {
pub fn index(&self) -> usize {
self.index
}
pub fn codec(&self) -> Codec {
self.codec.clone()
}
}
impl fmt::Display for Stream {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"index: {}\ncodec_type: {}\ncodec: {}",
self.index,
if let Some(typ) = &self.codec_type {
typ
} else {
"NONE"
},
if let Some(codec) = &self.codec {
codec
} else {
"NONE"
}
)
}
}
type Streams = Vec<Stream>;
#[derive(serde::Deserialize)]
pub struct FfProbe {
pub streams: Streams,
}
pub struct Metadata {
times: Vec<Time>,
key_frames: Vec<Frame>,
streams: Vec<Stream>,
}
impl Metadata {
pub fn new<P>(video: P) -> anyhow::Result<Self>
where
P: AsRef<Path>,
{
trace!("Retrieving metadata ...");
let streams = retrieve_stream_info(&video)?;
{
let mut has_audio_stream = false;
let mut has_video_stream = false;
for stream in &streams {
if let Some(typ) = &stream.codec_type {
if StreamType::from_str(typ).unwrap() == StreamType::Audio {
if stream.codec.is_none() {
return Err(anyhow!(
"Audio stream {} has no codec assigned",
stream.index
));
}
has_audio_stream = true
}
}
if let Some(typ) = &stream.codec_type {
if StreamType::from_str(typ).unwrap() == StreamType::Video {
if stream.codec.is_none() {
return Err(anyhow!(
"Video stream {} has no codec assigned",
stream.index
));
}
has_video_stream = true
}
}
}
if !has_audio_stream && !has_video_stream {
return Err(anyhow!("Video neither has a video nor an audio stream"));
}
}
let (times, key_frames) = retrieve_indexes(video)?;
trace!("Metadata retrieved");
Ok(Metadata {
times,
key_frames,
streams,
})
}
pub fn streams(&self) -> std::slice::Iter<'_, Stream> {
self.streams.iter()
}
pub fn has_frames(&self) -> bool {
!self.times.is_empty()
}
pub fn frame_to_time(&self, frame: Frame) -> anyhow::Result<Time> {
if frame >= Frame::from(self.times.len()) {
Err(anyhow!(
"Video does not contain a frame with number {}",
frame
))
} else {
Ok(self.times[usize::from(frame)])
}
}
pub fn time_to_frame(&self, time: Time) -> anyhow::Result<Frame> {
if self.times.is_empty() {
return Err(anyhow!(
"Cannot determine frame number of time {} since there are no frames",
time
));
}
Ok(Frame::from(match self.times.binary_search(&time) {
Ok(i) => i,
Err(i) => {
if i == self.times.len() {
i - 1
} else {
i
}
}
}))
}
pub fn key_frame_greater_or_equal_until_limit(
&self,
frame: Frame,
limit: Frame,
) -> Option<Frame> {
if limit < frame {
panic!("Limit must be greater than or equal to frame")
}
match self.key_frames.binary_search(&frame) {
Ok(_) => Some(frame),
Err(i) => {
if i < self.key_frames.len() && self.key_frames[i] <= limit {
Some(self.key_frames[i])
} else {
None
}
}
}
}
pub fn key_frame_less_or_equal_until_limit(&self, frame: Frame, limit: Frame) -> Option<Frame> {
if limit > frame {
panic!("Limit must be less than or equal to frame")
}
match self.key_frames.binary_search(&frame) {
Ok(_) => Some(frame),
Err(i) => {
if i > 0 && self.key_frames[i - 1] >= limit {
Some(self.key_frames[i - 1])
} else {
None
}
}
}
}
}
fn retrieve_indexes<P>(video: P) -> anyhow::Result<(Vec<Time>, Vec<Frame>)>
where
P: AsRef<Path>,
{
defer! {
for ext in [FFMS2_INDEX_EXT,FFMS2_KEY_FRAMES_INDEX_EXT,FFMS2_TIMES_INDEX_EXT].iter() {
_ = fs::remove_file(Path::new(&format!(
"{}.{}",
video.as_ref().display(),
ext
)));
}
trace!("Removed FFMS2 index files");
}
trace!("Retrieving data from FFMS2 index ...");
let output: Output = cmd!("ffmsindex", "-f", "-k", "-c", video.as_ref().as_os_str(),)
.stdout_null()
.stderr_capture()
.unchecked()
.run()
.context("Could not execute ffmsindex to create index files")?;
if !output.status.success() {
return Err(anyhow!("ffmsindex: {}", from_utf8(&output.stderr).unwrap())
.context("Could not create key frame / time indexes"));
}
let mut times: Vec<Time> = vec![];
if let Ok(file) = File::open(Path::new(&format!(
"{}.{}",
video.as_ref().display(),
FFMS2_TIMES_INDEX_EXT
))) {
for line in BufReader::new(file).lines().skip(1).flatten() {
times.push(Time::from(
&line
.parse::<f64>()
.context(format!("Could not convert \"{}\" into time", line))?
/ 1000.0,
));
}
} else {
debug!("Times index file does not exist");
}
let mut key_frames: Vec<Frame> = vec![];
if let Ok(file) = File::open(Path::new(&format!(
"{}.{}",
video.as_ref().display(),
FFMS2_KEY_FRAMES_INDEX_EXT
))) {
for line in BufReader::new(file).lines().skip(2).flatten() {
key_frames.push(Frame::from_str(&line)?);
}
} else {
debug!("Key frames index file does not exist");
}
trace!("Retrieved data from FFMS2 index");
Ok((times, key_frames))
}
fn retrieve_stream_info<P>(video: P) -> anyhow::Result<Streams>
where
P: AsRef<Path>,
{
let output = cmd!(
"ffprobe",
"-loglevel",
"0",
"-show_streams",
"-print_format",
"json",
video.as_ref().as_os_str()
)
.stderr_capture()
.unchecked()
.read()
.with_context(|| "Could not execute ffprobe to retrieve stream information")?;
let ffprobe: FfProbe =
serde_json::from_str(&output).with_context(|| "Could not parse stream information")?;
Ok(ffprobe.streams)
}