use std::collections::BTreeMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use crate::error::MediaError;
pub type MediaSinkFuture<'a> = Pin<Box<dyn Future<Output = Result<(), MediaError>> + Send + 'a>>;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MediaArtifactKind {
Thumbnail,
Preview,
Transcode,
}
impl MediaArtifactKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Thumbnail => "thumbnail",
Self::Preview => "preview",
Self::Transcode => "transcode",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct MediaArtifactFile {
pub key: String,
pub storage_uri: String,
pub public_url: String,
pub content_type: String,
pub bytes: u64,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct MediaArtifact {
pub kind: MediaArtifactKind,
pub workflow_id: String,
pub source_id: Option<String>,
pub primary: MediaArtifactFile,
pub secondary: Option<MediaArtifactFile>,
pub metadata: BTreeMap<String, String>,
}
pub trait MediaArtifactSink: Send + Sync + 'static {
fn on_completed(&self, artifact: MediaArtifact) -> MediaSinkFuture<'_>;
fn on_failed(
&self,
kind: MediaArtifactKind,
workflow_id: String,
error: String,
) -> MediaSinkFuture<'_>;
}
#[derive(Clone)]
pub struct MediaArtifactSinkExt(pub Arc<dyn MediaArtifactSink>);
#[cfg(test)]
pub(crate) mod tests {
use super::{
MediaArtifact, MediaArtifactFile, MediaArtifactKind, MediaArtifactSink, MediaSinkFuture,
};
use std::collections::BTreeMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
pub struct CountingSink {
pub completed: AtomicUsize,
pub failed: AtomicUsize,
pub last_key: std::sync::Mutex<Option<String>>,
}
impl CountingSink {
pub fn new() -> Self {
Self {
completed: AtomicUsize::new(0),
failed: AtomicUsize::new(0),
last_key: std::sync::Mutex::new(None),
}
}
}
impl MediaArtifactSink for CountingSink {
fn on_completed(&self, artifact: MediaArtifact) -> MediaSinkFuture<'_> {
self.completed.fetch_add(1, Ordering::SeqCst);
*self.last_key.lock().expect("lock") = Some(artifact.primary.key);
Box::pin(async { Ok(()) })
}
fn on_failed(
&self,
_kind: MediaArtifactKind,
_workflow_id: String,
_error: String,
) -> MediaSinkFuture<'_> {
self.failed.fetch_add(1, Ordering::SeqCst);
Box::pin(async { Ok(()) })
}
}
fn sample_artifact() -> MediaArtifact {
MediaArtifact {
kind: MediaArtifactKind::Thumbnail,
workflow_id: "wf-1".to_owned(),
source_id: Some("stream-7".to_owned()),
primary: MediaArtifactFile {
key: "thumbnails/7.jpg".to_owned(),
storage_uri: "/media/thumbnails/7.jpg".to_owned(),
public_url: "/media/thumbnails/7.jpg".to_owned(),
content_type: "image/jpeg".to_owned(),
bytes: 2048,
},
secondary: None,
metadata: BTreeMap::new(),
}
}
#[test]
fn artifact_kind_labels_are_stable() {
assert_eq!(MediaArtifactKind::Thumbnail.as_str(), "thumbnail");
assert_eq!(MediaArtifactKind::Preview.as_str(), "preview");
assert_eq!(MediaArtifactKind::Transcode.as_str(), "transcode");
}
#[test]
fn artifact_round_trips_through_json() {
let artifact = sample_artifact();
let json = serde_json::to_string(&artifact).expect("serialize");
let back: MediaArtifact = serde_json::from_str(&json).expect("deserialize");
assert_eq!(artifact, back);
}
#[tokio::test]
async fn sink_on_completed_receives_the_artifact() {
let sink = Arc::new(CountingSink::new());
sink.on_completed(sample_artifact()).await.expect("ok");
assert_eq!(sink.completed.load(Ordering::SeqCst), 1);
assert_eq!(sink.failed.load(Ordering::SeqCst), 0);
assert_eq!(
sink.last_key.lock().expect("lock").as_deref(),
Some("thumbnails/7.jpg")
);
}
}