pulse-pixelstream-types 0.13.10

Shared Myko entity and command types for the Pulse Pixelstream recording cell.
Documentation
//! Read-only compatibility schema for pre-RecordingJob persistence.

use myko::prelude::*;
use myko::TS;
use myko_macros::myko_item;
use serde::{Deserialize, Serialize};

use crate::{
    Capture, CaptureIntegrity, CaptureQuality, CollectionPathSegment, CreativeStatus,
    DeliveryStatus, QcStatus, RecordingJob, RecordingJobId, RecordingJobPhase, Representation,
    RepresentationKind, ShotDirection, ShotEntryPlan, Take, TakeState,
};

#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case")]
pub enum LegacyCaptureAttemptState {
    #[default]
    Recording,
    Recorded,
    QualityWarning,
    RetryingSourceLoss,
    ReshootRequired,
    Failed,
    Delivered,
    Accepted,
}

#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
pub struct LegacyCaptureAttempt {
    pub attempt_id: String,
    #[serde(default, alias = "cueId")]
    pub clip_id: String,
    pub shot_id: String,
    #[serde(default)]
    pub shot_name: String,
    pub direction: ShotDirection,
    pub attempt_number: u32,
    pub state: LegacyCaptureAttemptState,
    #[serde(default)]
    pub quality: CaptureQuality,
    #[serde(default)]
    pub file_name: String,
    #[serde(default)]
    pub error: String,
    #[serde(default)]
    pub started_at_ms: u64,
    #[serde(default)]
    pub updated_at_ms: u64,
}

#[myko_item]
pub struct CaptureRun {
    pub run_id: String,
    pub streamer_id: String,
    #[serde(default, alias = "shotListId")]
    pub timeline_id: String,
    #[serde(default, alias = "shotListName")]
    pub timeline_name: String,
    #[serde(default, alias = "shotListVersion")]
    pub timeline_version: u32,
    #[serde(default)]
    pub collection_id: String,
    #[serde(default)]
    pub collection_path: Vec<CollectionPathSegment>,
    #[serde(default)]
    pub phase: RecordingJobPhase,
    #[serde(default, alias = "items")]
    pub shots: Vec<ShotEntryPlan>,
    #[serde(default)]
    pub attempts: Vec<LegacyCaptureAttempt>,
    #[serde(default)]
    pub error: String,
    #[serde(default)]
    pub started_at_ms: u64,
    #[serde(default)]
    pub updated_at_ms: u64,
    #[serde(default)]
    pub elapsed_ms: u64,
    #[serde(default)]
    pub estimated_total_ms: u64,
    #[serde(default)]
    pub estimated_remaining_ms: u64,
}

impl CaptureRun {
    pub fn to_recording_job(&self) -> RecordingJob {
        let job_id = if self.run_id.trim().is_empty() {
            self.id.to_string()
        } else {
            self.run_id.clone()
        };
        RecordingJob {
            id: RecordingJobId::from(job_id.clone()),
            job_id,
            capture_kind: crate::RecordingKind::Video,
            streamer_id: self.streamer_id.clone(),
            capture_context: None,
            timeline_id: self.timeline_id.clone(),
            timeline_name: self.timeline_name.clone(),
            timeline_revision: self.timeline_version,
            collection_id: self.collection_id.clone(),
            collection_path: self.collection_path.clone(),
            phase: self.phase.clone(),
            pause_requested: false,
            entries: self.shots.clone(),
            takes: self.attempts.iter().map(legacy_take).collect(),
            error: self.error.clone(),
            started_at_ms: self.started_at_ms,
            updated_at_ms: self.updated_at_ms,
            elapsed_ms: self.elapsed_ms,
            estimated_total_ms: self.estimated_total_ms,
            estimated_remaining_ms: self.estimated_remaining_ms,
        }
    }
}

fn legacy_take(attempt: &LegacyCaptureAttempt) -> Take {
    let state = match attempt.state {
        LegacyCaptureAttemptState::Recording => TakeState::Recording,
        LegacyCaptureAttemptState::Failed => TakeState::Failed,
        _ => TakeState::Completed,
    };
    let has_capture = !matches!(
        attempt.state,
        LegacyCaptureAttemptState::Recording | LegacyCaptureAttemptState::Failed
    ) || !attempt.file_name.trim().is_empty();
    let capture = has_capture.then(|| {
        let integrity = if !attempt.quality.structurally_valid {
            CaptureIntegrity::Corrupt
        } else if attempt.quality.source_transport_damage {
            CaptureIntegrity::Damaged
        } else {
            CaptureIntegrity::Complete
        };
        let qc_status = if matches!(integrity, CaptureIntegrity::Corrupt)
            || attempt.state == LegacyCaptureAttemptState::ReshootRequired
        {
            QcStatus::Failed
        } else if attempt.quality.advisory {
            QcStatus::Warning
        } else {
            QcStatus::Clean
        };
        let delivery_status = if matches!(
            attempt.state,
            LegacyCaptureAttemptState::Delivered
                | LegacyCaptureAttemptState::Accepted
                | LegacyCaptureAttemptState::ReshootRequired
        ) {
            DeliveryStatus::Delivered
        } else {
            DeliveryStatus::Pending
        };
        let representations = (!attempt.file_name.trim().is_empty())
            .then(|| Representation {
                representation_id: format!("{}:delivery", attempt.attempt_id),
                kind: RepresentationKind::Delivery,
                codec: "h264".to_owned(),
                container: "mp4".to_owned(),
                file_name: attempt.file_name.clone(),
            })
            .into_iter()
            .collect();
        Capture {
            capture_id: format!("{}:capture", attempt.attempt_id),
            integrity,
            qc_status,
            creative_status: if attempt.state == LegacyCaptureAttemptState::Accepted {
                CreativeStatus::Accepted
            } else {
                CreativeStatus::Unreviewed
            },
            delivery_status,
            quality: attempt.quality.clone(),
            representations,
        }
    });
    Take {
        take_id: attempt.attempt_id.clone(),
        entry_id: attempt.clip_id.clone(),
        shot_id: attempt.shot_id.clone(),
        shot_name: attempt.shot_name.clone(),
        direction: attempt.direction,
        take_number: attempt.attempt_number,
        state,
        capture,
        error: attempt.error.clone(),
        started_at_ms: attempt.started_at_ms,
        updated_at_ms: attempt.updated_at_ms,
    }
}