nannou_video 0.20.0

Video decoding and playback, built on Bevy for nannou - the creative-coding framework.
Documentation
use bevy::asset::RenderAssetUsages;
use bevy::prelude::*;
use bevy::render::render_resource::{Extent3d, TextureDimension, TextureFormat};

use crate::asset::Video;
use crate::components::{SeekTo, VideoOutput, VideoPlayer};
use crate::events::{VideoEnded, VideoFailed, VideoLoaded, VideoLooped, VideoSeeked};
use crate::worker::{
    FrameEvent, FramePayload, PlayerCommand, VideoWorker, WorkerConfig, spawn_worker,
};

#[derive(Component)]
pub(crate) struct PendingVideo;

pub(crate) fn attach_workers(
    mut commands: Commands,
    mut images: ResMut<Assets<Image>>,
    videos: Res<Assets<Video>>,
    added: Query<(Entity, &VideoPlayer), (Added<VideoPlayer>, Without<VideoWorker>)>,
    pending: Query<(Entity, &VideoPlayer), (With<PendingVideo>, Without<VideoWorker>)>,
) {
    for (entity, player) in added.iter().chain(pending.iter()) {
        let Some(video) = videos.get(&player.video) else {
            commands.entity(entity).insert(PendingVideo);
            continue;
        };
        let image = Image::new_fill(
            Extent3d {
                width: video.size.x,
                height: video.size.y,
                ..default()
            },
            TextureDimension::D2,
            &[0, 0, 0, 255],
            TextureFormat::Rgba8UnormSrgb,
            RenderAssetUsages::default(),
        );
        let image_handle = images.add(image);
        let worker = spawn_worker(WorkerConfig {
            source: video.source.clone(),
            mode: player.mode,
            hw_accel: player.hw_accel.resolve(),
            resize: player.resize.resolve(),
            options: video.options.clone(),
        });
        if player.paused {
            let _ = worker.cmd_tx.send(PlayerCommand::Pause);
        }
        if (player.speed - 1.0).abs() > f32::EPSILON {
            let _ = worker.cmd_tx.send(PlayerCommand::SetSpeed(player.speed));
        }
        commands.entity(entity).insert((
            VideoOutput {
                image: image_handle,
                size: video.size,
                position_seconds: 0.0,
            },
            worker,
        ));
        commands.entity(entity).remove::<PendingVideo>();
        commands
            .entity(entity)
            .trigger(|e| VideoLoaded { entity: e });
    }
}

pub(crate) fn process_seeks(mut commands: Commands, seeks: Query<(Entity, &VideoWorker, &SeekTo)>) {
    for (entity, worker, seek) in &seeks {
        let _ = worker.cmd_tx.send(PlayerCommand::Seek(seek.0));
        commands.entity(entity).remove::<SeekTo>();
        commands.entity(entity).trigger(|e| VideoSeeked {
            entity: e,
            to_seconds: seek.0,
        });
    }
}

pub(crate) fn sync_commands(players: Query<(&VideoPlayer, &VideoWorker), Changed<VideoPlayer>>) {
    for (player, worker) in &players {
        let _ = worker.cmd_tx.send(if player.paused {
            PlayerCommand::Pause
        } else {
            PlayerCommand::Play
        });
        let _ = worker.cmd_tx.send(PlayerCommand::SetSpeed(player.speed));
        let _ = worker.cmd_tx.send(PlayerCommand::SetMode(player.mode));
    }
}

pub(crate) fn drain_frames(
    mut commands: Commands,
    mut images: ResMut<Assets<Image>>,
    mut players: Query<(Entity, &VideoWorker, &mut VideoOutput)>,
) {
    for (entity, worker, mut output) in &mut players {
        let mut latest_frame: Option<FramePayload> = None;
        while let Ok(event) = worker.frame_rx.try_recv() {
            match event {
                FrameEvent::Frame(payload) => {
                    latest_frame = Some(payload);
                }
                FrameEvent::Ended => {
                    commands
                        .entity(entity)
                        .trigger(|e| VideoEnded { entity: e });
                }
                FrameEvent::Looped => {
                    commands
                        .entity(entity)
                        .trigger(|e| VideoLooped { entity: e });
                }
                FrameEvent::Error(reason) => {
                    commands
                        .entity(entity)
                        .trigger(|e| VideoFailed { entity: e, reason });
                }
            }
        }
        if let Some(payload) = latest_frame {
            if let Some(mut image) = images.get_mut(&output.image) {
                write_frame(&mut image, payload.size, payload.pixels);
            }
            output.size = payload.size;
            output.position_seconds = payload.pts_seconds;
        }
    }
}

pub(crate) fn on_player_removed(event: On<Remove, VideoPlayer>, mut commands: Commands) {
    let entity = event.event_target();
    commands
        .entity(entity)
        .remove::<(VideoWorker, VideoOutput, PendingVideo, SeekTo)>();
}

fn write_frame(image: &mut Image, size: UVec2, pixels: Vec<u8>) {
    let extent = Extent3d {
        width: size.x,
        height: size.y,
        depth_or_array_layers: 1,
    };
    if image.texture_descriptor.size != extent {
        image.resize(extent);
    }
    image.data = Some(pixels);
}